Databsae service refactoring. Warning, loses stars and listens stats
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
add_subdirectory(auth)
|
add_subdirectory(auth)
|
||||||
add_subdirectory(cover)
|
add_subdirectory(cover)
|
||||||
add_subdirectory(database)
|
add_subdirectory(database)
|
||||||
add_subdirectory(feedback)
|
|
||||||
add_subdirectory(recommendation)
|
add_subdirectory(recommendation)
|
||||||
add_subdirectory(scanner)
|
add_subdirectory(scanner)
|
||||||
add_subdirectory(scrobbling)
|
add_subdirectory(scrobbling)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace Auth
|
|||||||
Session& session {getDbSession()};
|
Session& session {getDbSession()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
User::pointer user {User::getByLoginName(session, loginName)};
|
User::pointer user {User::find(session, loginName)};
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
||||||
@@ -58,7 +58,7 @@ namespace Auth
|
|||||||
Session& session {getDbSession()};
|
Session& session {getDbSession()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
User::pointer user {User::getById(session, userId)};
|
User::pointer user {User::find(session, userId)};
|
||||||
if (user)
|
if (user)
|
||||||
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
|
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <Wt/WRandom.h>
|
#include <Wt/WRandom.h>
|
||||||
|
|
||||||
#include "services/auth/Types.hpp"
|
#include "services/auth/Types.hpp"
|
||||||
|
#include "services/database/AuthToken.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
@@ -55,7 +56,7 @@ namespace Auth
|
|||||||
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
Database::User::pointer user {Database::User::find(session, userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
throw Exception {"User deleted"};
|
throw Exception {"User deleted"};
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ namespace Auth
|
|||||||
Database::Session& session {getDbSession()};
|
Database::Session& session {getDbSession()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)};
|
Database::AuthToken::pointer authToken {Database::AuthToken::find(session, secretHash)};
|
||||||
if (!authToken)
|
if (!authToken)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ namespace Auth
|
|||||||
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
Database::User::pointer user {Database::User::find(session, userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
throw Exception {"User deleted"};
|
throw Exception {"User deleted"};
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace Auth
|
|||||||
Database::Session& session {getDbSession()};
|
Database::Session& session {getDbSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
|
const Database::User::pointer user {Database::User::find(session, loginName)};
|
||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
|
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
|
||||||
@@ -102,7 +102,7 @@ namespace Auth
|
|||||||
Database::Session& session {getDbSession()};
|
Database::Session& session {getDbSession()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
Database::User::pointer user {Database::User::find(session, userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
throw Exception {"User not found!"};
|
throw Exception {"User not found!"};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
#include <boost/asio/ip/address.hpp>
|
#include <boost/asio/ip/address.hpp>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
#include <Wt/Dbo/ptr.h>
|
#include <Wt/Dbo/ptr.h>
|
||||||
|
|
||||||
#include "services/auth/Types.hpp"
|
#include "services/auth/Types.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace
|
|||||||
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
auto transaction {dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(dbSession, trackId)};
|
const Database::Track::pointer track {Database::Track::find(dbSession, trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
|
|||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)})
|
if (const Database::Release::pointer release {Database::Release::find(session, releaseId)})
|
||||||
{
|
{
|
||||||
if (const auto firstTrack {release->getFirstTrack()})
|
if (const auto firstTrack {release->getFirstTrack()})
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
add_library(lmsdatabase SHARED
|
add_library(lmsdatabase SHARED
|
||||||
impl/Artist.cpp
|
impl/Artist.cpp
|
||||||
|
impl/AuthToken.cpp
|
||||||
impl/Cluster.cpp
|
impl/Cluster.cpp
|
||||||
impl/Db.cpp
|
impl/Db.cpp
|
||||||
|
impl/Listen.cpp
|
||||||
impl/TrackArtistLink.cpp
|
impl/TrackArtistLink.cpp
|
||||||
impl/TrackFeatures.cpp
|
impl/TrackFeatures.cpp
|
||||||
impl/TrackList.cpp
|
impl/TrackList.cpp
|
||||||
impl/Release.cpp
|
impl/Release.cpp
|
||||||
impl/ScanSettings.cpp
|
impl/ScanSettings.cpp
|
||||||
impl/Session.cpp
|
impl/Session.cpp
|
||||||
|
impl/StarredArtist.cpp
|
||||||
|
impl/StarredRelease.cpp
|
||||||
|
impl/StarredTrack.cpp
|
||||||
impl/SqlQuery.cpp
|
impl/SqlQuery.cpp
|
||||||
impl/Track.cpp
|
impl/Track.cpp
|
||||||
impl/TrackBookmark.cpp
|
impl/TrackBookmark.cpp
|
||||||
|
impl/Types.cpp
|
||||||
impl/User.cpp
|
impl/User.cpp
|
||||||
impl/Utils.cpp
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "Traits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -40,8 +40,16 @@ _MBID {MBID ? MBID->getAsString() : ""}
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Artist::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
Artist::getByName(Session& session, const std::string& name)
|
Artist::find(Session& session, const std::string& name)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -53,14 +61,14 @@ Artist::getByName(Session& session, const std::string& name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Artist::pointer
|
Artist::pointer
|
||||||
Artist::getByMBID(Session& session, const UUID& mbid)
|
Artist::find(Session& session, const UUID& mbid)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
|
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
Artist::pointer
|
Artist::pointer
|
||||||
Artist::getById(Session& session, ArtistId id)
|
Artist::find(Session& session, ArtistId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||||
@@ -84,36 +92,37 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
static
|
static
|
||||||
Wt::Dbo::Query<T>
|
Wt::Dbo::Query<ArtistId>
|
||||||
createQuery(Session& session,
|
createQuery(Session& session, const Artist::FindParameters& params)
|
||||||
const std::string& queryStr,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
const std::vector<std::string_view>& keywords,
|
|
||||||
std::optional<TrackArtistLinkType> linkType)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto query {session.getDboSession().query<T>(queryStr)};
|
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
|
||||||
|
if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType)
|
||||||
|
{
|
||||||
query.join("track t ON t.id = t_a_l.track_id");
|
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("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||||
|
}
|
||||||
|
|
||||||
if (linkType)
|
if (params.linkType)
|
||||||
query.where("t_a_l.type = ?").bind(*linkType);
|
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||||
|
|
||||||
if (!keywords.empty())
|
if (params.writtenAfter.isValid())
|
||||||
|
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||||
|
|
||||||
|
if (!params.keywords.empty())
|
||||||
{
|
{
|
||||||
std::vector<std::string> clauses;
|
std::vector<std::string> clauses;
|
||||||
std::vector<std::string> sortClauses;
|
std::vector<std::string> sortClauses;
|
||||||
|
|
||||||
for (std::string_view keyword : keywords)
|
for (std::string_view keyword : params.keywords)
|
||||||
{
|
{
|
||||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||||
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (std::string_view keyword : keywords)
|
for (std::string_view keyword : params.keywords)
|
||||||
{
|
{
|
||||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||||
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||||
@@ -122,7 +131,15 @@ createQuery(Session& session,
|
|||||||
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (params.starringUser.isValid())
|
||||||
|
{
|
||||||
|
assert(params.scrobbler);
|
||||||
|
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||||
|
.where("s_a.user_id = ?").bind(params.starringUser)
|
||||||
|
.where("s_a.scrobbler = ?").bind(*params.scrobbler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.clusters.empty())
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||||
@@ -132,268 +149,59 @@ createQuery(Session& session,
|
|||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
WhereClause clusterClause;
|
WhereClause clusterClause;
|
||||||
for (const ClusterId clusterId : clusterIds)
|
for (const ClusterId clusterId : params.clusters)
|
||||||
{
|
{
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
query.bind(clusterId);
|
query.bind(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
oss << " " << clusterClause.get();
|
||||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
|
||||||
|
|
||||||
query.where(oss.str());
|
query.where(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (params.sortMethod)
|
||||||
|
{
|
||||||
|
case ArtistSortMethod::None:
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::ByName:
|
||||||
|
query.orderBy("a.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::BySortName:
|
||||||
|
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::Random:
|
||||||
|
query.orderBy("RANDOM()");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::LastWritten:
|
||||||
|
query.orderBy("t.file_last_write DESC");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::StarredDateDesc:
|
||||||
|
assert(params.starringUser.isValid());
|
||||||
|
query.orderBy("s_a.date_time DESC");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
RangeResults<ArtistId>
|
||||||
Artist::getAll(Session& session)
|
Artist::findAllOrphans(Session& session, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ArtistId>
|
||||||
|
Artist::find(Session& session, const FindParameters& params)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>();
|
auto query {createQuery(session, params)};
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return execQuery(query, params.range);
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
std::vector<Release::pointer>
|
||||||
@@ -463,32 +271,19 @@ Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
|
|||||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
RangeResults<Track::pointer>
|
||||||
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const
|
||||||
{
|
{
|
||||||
assert(session());
|
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")
|
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("a.id = ?").bind(getId())
|
||||||
.where("t.release_id is NULL")
|
.where("t.release_id is NULL")
|
||||||
.orderBy("t.name")
|
.orderBy("t.name")};
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
|
||||||
|
|
||||||
if (linkType)
|
if (linkType)
|
||||||
query.where("t_a_l.type = ?").bind(*linkType);
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {query.resultList()};
|
return execQuery(query, range);
|
||||||
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
|
bool
|
||||||
@@ -518,14 +313,14 @@ Artist::getRandomTracks(std::optional<std::size_t> count) const
|
|||||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
RangeResults<ArtistId>
|
||||||
Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss <<
|
oss <<
|
||||||
"SELECT a FROM artist a"
|
"SELECT a.id FROM artist a"
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
" INNER JOIN track_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 t ON t.id = t_a_l.track_id"
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||||
@@ -554,19 +349,15 @@ Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::opt
|
|||||||
oss << ")";
|
oss << ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>> query {session()->query<Wt::Dbo::ptr<Artist>>(oss.str())
|
auto query {session()->query<ArtistId>(oss.str())
|
||||||
.bind(getId())
|
.bind(getId())
|
||||||
.bind(getId())
|
.bind(getId())
|
||||||
.groupBy("a.id")
|
.groupBy("a.id")
|
||||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
.orderBy("COUNT(*) DESC, RANDOM()")};
|
||||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
|
||||||
|
|
||||||
for (TrackArtistLinkType type : artistLinkTypes)
|
for (TrackArtistLinkType type : artistLinkTypes)
|
||||||
query.bind(type);
|
query.bind(type);
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {query.resultList()};
|
return execQuery(query, range);
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>>
|
std::vector<std::vector<Cluster::pointer>>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "services/database/AuthToken.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "StringViewTraits.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
|
||||||
|
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||||
|
: _value {value}
|
||||||
|
, _expiry {expiry}
|
||||||
|
, _user {getDboPtr(user)}
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthToken::pointer
|
||||||
|
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||||
|
{
|
||||||
|
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::find(Session& session, std::string_view value)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().find<AuthToken>()
|
||||||
|
.where("value = ?").bind(value)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,9 @@
|
|||||||
#include "services/database/ScanSettings.hpp"
|
#include "services/database/ScanSettings.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Traits.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -35,6 +36,14 @@ Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Cluster::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||||
|
}
|
||||||
|
|
||||||
Cluster::pointer
|
Cluster::pointer
|
||||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||||
{
|
{
|
||||||
@@ -46,25 +55,26 @@ Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Cluster::pointer>
|
RangeResults<ClusterId>
|
||||||
Cluster::getAll(Session& session)
|
Cluster::find(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> res {session.getDboSession().find<Cluster>()};
|
return execQuery(query, range);
|
||||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Cluster::pointer>
|
RangeResults<ClusterId>
|
||||||
Cluster::getAllOrphans(Session& session)
|
Cluster::findOrphans(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
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()};
|
auto query {session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")};
|
||||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
|
||||||
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cluster::pointer
|
Cluster::pointer
|
||||||
Cluster::getById(Session& session, ClusterId id)
|
Cluster::find(Session& session, ClusterId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -77,29 +87,15 @@ Cluster::addTrack(ObjectPtr<Track> track)
|
|||||||
_tracks.insert(getDboPtr(track));
|
_tracks.insert(getDboPtr(track));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
RangeResults<TrackId>
|
||||||
Cluster::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
|
Cluster::getTracks(Range range) const
|
||||||
{
|
{
|
||||||
assert(session());
|
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")
|
auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||||
.where("c.id = ?").bind(getId())
|
.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());
|
return execQuery(query, range);
|
||||||
}
|
|
||||||
|
|
||||||
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
|
std::size_t
|
||||||
@@ -117,33 +113,42 @@ ClusterType::ClusterType(std::string_view name)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ClusterType::pointer>
|
std::size_t
|
||||||
ClusterType::getAllOrphans(Session& session)
|
ClusterType::getCount(Session& session)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||||
"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)
|
RangeResults<ClusterTypeId>
|
||||||
|
ClusterType::findOrphans(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
auto query {session.getDboSession().query<ClusterTypeId>(
|
||||||
"SELECT DISTINCT c_t from cluster_type c_t")
|
"SELECT c_t.id from cluster_type c_t"
|
||||||
.join("cluster c ON c_t.id = c.cluster_type_id");
|
" 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());
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ClusterTypeId>
|
||||||
|
ClusterType::findUsed(Session& session, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<ClusterTypeId>(
|
||||||
|
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||||
|
.join("cluster c ON c_t.id = c.cluster_type_id")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer
|
||||||
ClusterType::getByName(Session& session, const std::string& name)
|
ClusterType::find(Session& session, const std::string& name)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -151,20 +156,21 @@ ClusterType::getByName(Session& session, const std::string& name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer
|
||||||
ClusterType::getById(Session& session, ClusterTypeId id)
|
ClusterType::find(Session& session, ClusterTypeId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ClusterType::pointer>
|
RangeResults<ClusterTypeId>
|
||||||
ClusterType::getAll(Session& session)
|
ClusterType::find(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().find<ClusterType>().resultList()};
|
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")};
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
|
||||||
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/Listen.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "SqlQuery.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Wt::Dbo::Query<ArtistId>
|
||||||
|
createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||||
|
{
|
||||||
|
auto query {session.query<ArtistId>("SELECT a.id from artist a")
|
||||||
|
.join("track t ON t.id = t_a_l.track_id")
|
||||||
|
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
|
||||||
|
.join("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||||
|
|
||||||
|
if (linkType)
|
||||||
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||||
|
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||||
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::Query<ReleaseId>
|
||||||
|
createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query {session.query<ReleaseId>("SELECT r.id from release r")
|
||||||
|
.join("track t ON t.release_id = r.id")
|
||||||
|
.join("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
|
" INNER JOIN track t ON t.release_id = r.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (ClusterId id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::Query<TrackId>
|
||||||
|
createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query {session.query<TrackId>("SELECT t.id from track t")
|
||||||
|
.join("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||||
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||||
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||||
|
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())}
|
||||||
|
, _scrobbler {scrobbler}
|
||||||
|
, _user {getDboPtr(user)}
|
||||||
|
, _track {getDboPtr(track)}
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
Listen::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
||||||
|
}
|
||||||
|
|
||||||
|
Listen::pointer
|
||||||
|
Listen::find(Session& session, ListenId id)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<Listen::pointer>
|
||||||
|
Listen::find(Session& session, UserId userId, Scrobbler scrobbler, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().find<Listen>()
|
||||||
|
.where("user_id = ?").bind(userId)
|
||||||
|
.where("scrobbler = ?").bind(scrobbler)
|
||||||
|
.orderBy("date_time")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
Listen::pointer
|
||||||
|
Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().find<Listen>()
|
||||||
|
.where("user_id = ?").bind(userId)
|
||||||
|
.where("track_id = ?").bind(trackId)
|
||||||
|
.where("scrobbler = ?").bind(scrobbler)
|
||||||
|
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
Listen::pointer
|
||||||
|
Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
|
Listen::pointer res {session.getDboSession().add(std::make_unique<Listen>(user, track, scrobbler, dateTime))};
|
||||||
|
session.getDboSession().flush();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ArtistId>
|
||||||
|
Listen::getTopArtists(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)};
|
||||||
|
|
||||||
|
auto collection {query
|
||||||
|
.orderBy("COUNT(a.id) DESC")
|
||||||
|
.groupBy("a.id")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ReleaseId>
|
||||||
|
Listen::getTopReleases(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
|
.orderBy("COUNT(r.id) DESC")
|
||||||
|
.groupBy("r.id")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackId>
|
||||||
|
Listen::getTopTracks(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
|
.orderBy("COUNT(t.id) DESC")
|
||||||
|
.groupBy("t.id")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ArtistId>
|
||||||
|
Listen::getRecentArtists(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
|
||||||
|
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||||
|
.orderBy("l.date_time DESC")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<ReleaseId>
|
||||||
|
Listen::getRecentReleases(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
|
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||||
|
.orderBy("l.date_time DESC")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackId>
|
||||||
|
Listen::getRecentTracks(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
|
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||||
|
.orderBy("l.date_time DESC")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -28,28 +28,45 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Traits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|
||||||
template <typename T>
|
Wt::Dbo::Query<ReleaseId>
|
||||||
static
|
createQuery(Session& session, const Release::FindParameters& params)
|
||||||
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<ReleaseId>("SELECT DISTINCT r.id from release r")};
|
||||||
|
|
||||||
auto query {session.getDboSession().query<T>(queryStr)};
|
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||||
|
|| params.writtenAfter.isValid()
|
||||||
|
|| params.dateRange)
|
||||||
|
{
|
||||||
query.join("track t ON t.release_id = r.id");
|
query.join("track t ON t.release_id = r.id");
|
||||||
|
}
|
||||||
|
|
||||||
for (std::string_view keyword : keywords)
|
if (params.writtenAfter.isValid())
|
||||||
|
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||||
|
|
||||||
|
if (params.dateRange)
|
||||||
|
{
|
||||||
|
query.where("t.date >= ?").bind(params.dateRange->begin);
|
||||||
|
query.where("t.date <= ?").bind(params.dateRange->end);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::string_view keyword : params.keywords)
|
||||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
|
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (params.starringUser.isValid())
|
||||||
|
{
|
||||||
|
assert(params.scrobbler);
|
||||||
|
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||||
|
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||||
|
.where("s_r.scrobbler = ?").bind(*params.scrobbler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.clusters.empty())
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
@@ -58,18 +75,40 @@ createQuery(Session& session,
|
|||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
WhereClause clusterClause;
|
WhereClause clusterClause;
|
||||||
for (const ClusterId clusterId : clusterIds)
|
for (const ClusterId clusterId : params.clusters)
|
||||||
{
|
{
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
query.bind(clusterId);
|
query.bind(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
oss << " " << clusterClause.get();
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||||
|
|
||||||
query.where(oss.str());
|
query.where(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (params.sortMethod)
|
||||||
|
{
|
||||||
|
case ReleaseSortMethod::None:
|
||||||
|
break;
|
||||||
|
case ReleaseSortMethod::Name:
|
||||||
|
query.orderBy("r.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ReleaseSortMethod::Random:
|
||||||
|
query.orderBy("RANDOM()");
|
||||||
|
break;
|
||||||
|
case ReleaseSortMethod::LastWritten:
|
||||||
|
query.orderBy("t.file_last_write DESC");
|
||||||
|
break;
|
||||||
|
case ReleaseSortMethod::Date:
|
||||||
|
query.orderBy("t.date, r.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ReleaseSortMethod::StarredDateDesc:
|
||||||
|
assert(params.starringUser.isValid());
|
||||||
|
query.orderBy("s_r.date_time DESC");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +119,7 @@ _MBID {MBID ? MBID->getAsString() : ""}
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
Release::getByName(Session& session, const std::string& name)
|
Release::find(Session& session, const std::string& name)
|
||||||
{
|
{
|
||||||
session.checkUniqueLocked();
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
@@ -93,7 +132,7 @@ Release::getByName(Session& session, const std::string& name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Release::pointer
|
Release::pointer
|
||||||
Release::getByMBID(Session& session, const UUID& mbid)
|
Release::find(Session& session, const UUID& mbid)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -104,7 +143,7 @@ Release::getByMBID(Session& session, const UUID& mbid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Release::pointer
|
Release::pointer
|
||||||
Release::getById(Session& session, ReleaseId id)
|
Release::find(Session& session, ReleaseId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -137,230 +176,44 @@ Release::getCount(Session& session)
|
|||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<Release>().resultList().size();
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
RangeResults<ReleaseId>
|
||||||
Release::getAll(Session& session, std::optional<Range> range)
|
Release::findOrderedByArtist(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().find<Release>()
|
// TODO merge with execQuery
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
auto query {session.getDboSession().query<ReleaseId>(
|
||||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
"SELECT DISTINCT r.id FROM release r"
|
||||||
.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 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 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")
|
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
|
||||||
.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());
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
RangeResults<ReleaseId>
|
||||||
Release::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
Release::findOrphans(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT DISTINCT r from release r", clusterIds, {})};
|
auto query {session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")};
|
||||||
auto res {query
|
return execQuery(query, range);
|
||||||
.orderBy("RANDOM()")
|
|
||||||
.limit(size ? static_cast<int>(*size) : -1)
|
|
||||||
.resultList()};
|
|
||||||
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ReleaseId>
|
RangeResults<ReleaseId>
|
||||||
Release::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
Release::find(Session& session, const FindParameters& params)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto query {createQuery<ReleaseId>(session, "SELECT DISTINCT r.id from release r", clusterIds, {})};
|
auto query {createQuery(session, params)};
|
||||||
|
|
||||||
Wt::Dbo::collection<ReleaseId> res = query
|
return execQuery(query, params.range);
|
||||||
.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>
|
std::optional<std::size_t>
|
||||||
Release::getTotalTrack(void) const
|
Release::getTotalTrack(void) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clu
|
|||||||
// Create any missing cluster type
|
// Create any missing cluster type
|
||||||
for (const std::string& clusterTypeName : clusterTypeNames)
|
for (const std::string& clusterTypeName : clusterTypeNames)
|
||||||
{
|
{
|
||||||
ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)};
|
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
|
||||||
if (!clusterType)
|
if (!clusterType)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
||||||
|
|||||||
@@ -28,10 +28,15 @@
|
|||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/AuthToken.hpp"
|
||||||
#include "services/database/Cluster.hpp"
|
#include "services/database/Cluster.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/ScanSettings.hpp"
|
#include "services/database/ScanSettings.hpp"
|
||||||
|
#include "services/database/StarredArtist.hpp"
|
||||||
|
#include "services/database/StarredRelease.hpp"
|
||||||
|
#include "services/database/StarredTrack.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackBookmark.hpp"
|
#include "services/database/TrackBookmark.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
@@ -43,7 +48,7 @@ namespace Database
|
|||||||
{
|
{
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
static constexpr Version LMS_DATABASE_VERSION {31};
|
static constexpr Version LMS_DATABASE_VERSION {32};
|
||||||
|
|
||||||
class VersionInfo
|
class VersionInfo
|
||||||
{
|
{
|
||||||
@@ -360,6 +365,61 @@ CREATE TABLE "track_backup" (
|
|||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(*this).modify()->incScanVersion();
|
ScanSettings::get(*this).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
else if (version == 31)
|
||||||
|
{
|
||||||
|
// new star system, using dedicated ObjectSets per scrobbler
|
||||||
|
_session.execute("DROP TABLE user_artist_starred");
|
||||||
|
_session.execute("DROP TABLE user_release_starred");
|
||||||
|
_session.execute("DROP TABLE user_track_starred");
|
||||||
|
|
||||||
|
_session.execute(R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "starred_artist" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"scrobbler" integer not null,
|
||||||
|
"date_time" text,
|
||||||
|
"artist_id" bigint,
|
||||||
|
"user_id" bigint,
|
||||||
|
constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
_session.execute(R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "starred_release" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"scrobbler" integer not null,
|
||||||
|
"date_time" text,
|
||||||
|
"release_id" bigint,
|
||||||
|
"user_id" bigint,
|
||||||
|
constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
_session.execute(R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "starred_track" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"scrobbler" integer not null,
|
||||||
|
"date_time" text,
|
||||||
|
"track_id" bigint,
|
||||||
|
"user_id" bigint,
|
||||||
|
constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
_session.execute(R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "listen" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"date_time" text,
|
||||||
|
"scrobbler" integer not null,
|
||||||
|
"track_id" bigint,
|
||||||
|
"user_id" bigint,
|
||||||
|
constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||||
@@ -380,8 +440,12 @@ Session::Session(Db& db)
|
|||||||
_session.mapClass<AuthToken>("auth_token");
|
_session.mapClass<AuthToken>("auth_token");
|
||||||
_session.mapClass<Cluster>("cluster");
|
_session.mapClass<Cluster>("cluster");
|
||||||
_session.mapClass<ClusterType>("cluster_type");
|
_session.mapClass<ClusterType>("cluster_type");
|
||||||
|
_session.mapClass<Listen>("listen");
|
||||||
_session.mapClass<Release>("release");
|
_session.mapClass<Release>("release");
|
||||||
_session.mapClass<ScanSettings>("scan_settings");
|
_session.mapClass<ScanSettings>("scan_settings");
|
||||||
|
_session.mapClass<StarredArtist>("starred_artist");
|
||||||
|
_session.mapClass<StarredRelease>("starred_release");
|
||||||
|
_session.mapClass<StarredTrack>("starred_track");
|
||||||
_session.mapClass<Track>("track");
|
_session.mapClass<Track>("track");
|
||||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||||
@@ -484,6 +548,11 @@ Session::prepareTables()
|
|||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_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_idx ON track_bookmark(user_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial settings tables
|
// Initial settings tables
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredArtist.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
: _scrobbler {scrobbler}
|
||||||
|
, _artist {getDboPtr(artist)}
|
||||||
|
, _user {getDboPtr(user)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
StarredArtist::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredArtist::pointer
|
||||||
|
StarredArtist::find(Session& session, StarredArtistId id)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredArtist::pointer
|
||||||
|
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredArtist>()
|
||||||
|
.where("artist_id = ?").bind(artistId)
|
||||||
|
.where("user_id = ?").bind(userId)
|
||||||
|
.where("scrobbler = ?").bind(scrobbler)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredArtist::pointer
|
||||||
|
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
|
StarredArtist::pointer res {session.getDboSession().add(std::make_unique<StarredArtist>(artist, user, scrobbler))};
|
||||||
|
session.getDboSession().flush();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
_dateTime = normalizeDateTime(dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredRelease.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
: _scrobbler {scrobbler}
|
||||||
|
, _release {getDboPtr(release)}
|
||||||
|
, _user {getDboPtr(user)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
StarredRelease::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredRelease::pointer
|
||||||
|
StarredRelease::find(Session& session, StarredReleaseId id)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredRelease::pointer
|
||||||
|
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredRelease>()
|
||||||
|
.where("release_id = ?").bind(releaseId)
|
||||||
|
.where("user_id = ?").bind(userId)
|
||||||
|
.where("scrobbler = ?").bind(scrobbler)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredRelease::pointer
|
||||||
|
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
|
StarredRelease::pointer res {session.getDboSession().add(std::make_unique<StarredRelease>(release, user, scrobbler))};
|
||||||
|
session.getDboSession().flush();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
_dateTime = normalizeDateTime(dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredTrack.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
: _scrobbler {scrobbler}
|
||||||
|
, _track {getDboPtr(track)}
|
||||||
|
, _user {getDboPtr(user)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
StarredTrack::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredTrack::pointer
|
||||||
|
StarredTrack::find(Session& session, StarredTrackId id)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredTrack::pointer
|
||||||
|
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
return session.getDboSession().find<StarredTrack>()
|
||||||
|
.where("track_id = ?").bind(trackId)
|
||||||
|
.where("user_id = ?").bind(userId)
|
||||||
|
.where("scrobbler = ?").bind(scrobbler)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredTrack::pointer
|
||||||
|
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||||
|
{
|
||||||
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
|
StarredTrack::pointer res {session.getDboSession().add(std::make_unique<StarredTrack>(track, user, scrobbler))};
|
||||||
|
session.getDboSession().flush();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
_dateTime = normalizeDateTime(dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,29 +30,36 @@
|
|||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "StringViewTraits.hpp"
|
#include "StringViewTraits.hpp"
|
||||||
#include "Traits.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
static
|
static
|
||||||
Wt::Dbo::Query<T>
|
Wt::Dbo::Query<TrackId>
|
||||||
createQuery(Session& session,
|
createQuery(Session& session, const Track::FindParameters& params)
|
||||||
const std::string& queryStr,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
const std::vector<std::string_view>& keywords)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto query {session.getDboSession().query<T>(queryStr)};
|
auto query {session.getDboSession().query<TrackId>("SELECT t.id from track t")};
|
||||||
|
|
||||||
for (std::string_view keyword : keywords)
|
for (std::string_view keyword : params.keywords)
|
||||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
|
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (params.writtenAfter.isValid())
|
||||||
|
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||||
|
|
||||||
|
if (params.starringUser.isValid())
|
||||||
|
{
|
||||||
|
assert(params.scrobbler);
|
||||||
|
query.join("starred_track s_t ON s_t.track_id = t.id")
|
||||||
|
.where("s_t.user_id = ?").bind(params.starringUser)
|
||||||
|
.where("s_t.scrobbler = ?").bind(*params.scrobbler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.clusters.empty())
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||||
@@ -60,18 +67,34 @@ createQuery(Session& session,
|
|||||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||||
|
|
||||||
WhereClause clusterClause;
|
WhereClause clusterClause;
|
||||||
for (const ClusterId clusterId : clusterIds)
|
for (const ClusterId clusterId : params.clusters)
|
||||||
{
|
{
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
query.bind(clusterId);
|
query.bind(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
oss << " " << clusterClause.get();
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||||
|
|
||||||
query.where(oss.str());
|
query.where(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (params.sortMethod)
|
||||||
|
{
|
||||||
|
case TrackSortMethod::None:
|
||||||
|
break;
|
||||||
|
case TrackSortMethod::LastWritten:
|
||||||
|
query.orderBy("t.file_last_write DESC");
|
||||||
|
break;
|
||||||
|
case TrackSortMethod::Random:
|
||||||
|
query.orderBy("RANDOM()");
|
||||||
|
break;
|
||||||
|
case TrackSortMethod::StarredDateDesc:
|
||||||
|
assert(params.starringUser.isValid());
|
||||||
|
query.orderBy("s_t.date_time DESC");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +103,17 @@ Track::Track(const std::filesystem::path& p)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::size_t
|
std::size_t
|
||||||
Track::getCount(Session& session)
|
Track::getCount(Session& session)
|
||||||
{
|
{
|
||||||
@@ -88,58 +122,8 @@ Track::getCount(Session& session)
|
|||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
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::pointer
|
||||||
Track::getByPath(Session& session, const std::filesystem::path& p)
|
Track::findByPath(Session& session, const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -147,7 +131,7 @@ Track::getByPath(Session& session, const std::filesystem::path& p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Track::pointer
|
Track::pointer
|
||||||
Track::getById(Session& session, TrackId id)
|
Track::find(Session& session, TrackId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -165,7 +149,7 @@ Track::exists(Session& session, TrackId id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
Track::findByRecordingMBID(Session& session, const UUID& mbid)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -176,151 +160,52 @@ Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
|||||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
Track::pointer
|
RangeResults<Track::PathResult>
|
||||||
Track::create(Session& session, const std::filesystem::path& p)
|
Track::findPaths(Session& session, Range range)
|
||||||
{
|
|
||||||
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>;
|
using QueryResultType = std::tuple<TrackId, std::string>;
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
|
// TODO Dbo traits on filesystem
|
||||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
auto query {session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track")};
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
|
||||||
|
|
||||||
std::vector<std::pair<TrackId, std::filesystem::path>> result;
|
RangeResults<QueryResultType> queryResults {execQuery(query, range)};
|
||||||
result.reserve(queryRes.size());
|
|
||||||
|
|
||||||
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
|
RangeResults<PathResult> res;
|
||||||
|
res.range = queryResults.range;
|
||||||
|
res.moreResults = queryResults.moreResults;
|
||||||
|
res.results.reserve(queryResults.results.size());
|
||||||
|
|
||||||
|
std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results),
|
||||||
[](const QueryResultType& queryResult)
|
[](const QueryResultType& queryResult)
|
||||||
{
|
{
|
||||||
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
|
return PathResult {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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
RangeResults<TrackId>
|
||||||
Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session)
|
Track::findMBIDDuplicates(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>
|
auto query {session.getDboSession().query<TrackId>( "SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)")
|
||||||
("SELECT t FROM track t")
|
.orderBy("track.release_id,track.disc_number,track.track_number,track.mbid")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackId>
|
||||||
|
Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<TrackId>("SELECT t.id FROM track t")
|
||||||
.where("LENGTH(t.recording_mbid) > 0")
|
.where("LENGTH(t.recording_mbid) > 0")
|
||||||
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")
|
.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>
|
return execQuery(query, range);
|
||||||
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>
|
std::vector<Cluster::pointer>
|
||||||
@@ -342,56 +227,31 @@ Track::getClusterIds() const
|
|||||||
return std::vector<ClusterId>(res.begin(), res.end());
|
return std::vector<ClusterId>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
RangeResults<TrackId>
|
||||||
Track::hasTrackFeatures() const
|
Track::find(Session& session, const FindParameters& parameters)
|
||||||
{
|
|
||||||
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();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto collection {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, keywords)
|
auto query {createQuery(session, parameters)};
|
||||||
.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());
|
return execQuery(query, parameters.range);
|
||||||
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>
|
RangeResults<TrackId>
|
||||||
Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
|
Track::findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
|
auto query {session.getDboSession().query<TrackId>("SELECT t.id from track t")
|
||||||
.join("release r ON t.release_id = r.id")
|
.join("release r ON t.release_id = r.id")
|
||||||
.where("t.name = ?").bind(trackName)
|
.where("t.name = ?").bind(trackName)
|
||||||
.where("r.name = ?").bind(releaseName)
|
.where("r.name = ?").bind(releaseName)};
|
||||||
.resultList()};
|
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
return execQuery(query, Range {});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
RangeResults<TrackId>
|
||||||
Track::getSimilarTracks(Session& session,
|
Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, Range range)
|
||||||
const std::vector<TrackId>& tracks,
|
|
||||||
std::optional<std::size_t> offset,
|
|
||||||
std::optional<std::size_t> size)
|
|
||||||
{
|
{
|
||||||
assert(!tracks.empty());
|
assert(!tracks.empty());
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
@@ -404,15 +264,13 @@ Track::getSimilarTracks(Session& session,
|
|||||||
oss << "?";
|
oss << "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto query {session.getDboSession().query<Wt::Dbo::ptr<Track>>(
|
auto query {session.getDboSession().query<TrackId>(
|
||||||
"SELECT t FROM track t"
|
"SELECT t.id FROM track t"
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
" 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_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() + ")")
|
" AND t.id NOT IN (" + oss.str() + ")")
|
||||||
.groupBy("t.id")
|
.groupBy("t.id")
|
||||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
.orderBy("COUNT(*) DESC, RANDOM()")};
|
||||||
.limit(size ? static_cast<int>(*size) : -1)
|
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
|
||||||
|
|
||||||
for (TrackId trackId : tracks)
|
for (TrackId trackId : tracks)
|
||||||
query.bind(trackId);
|
query.bind(trackId);
|
||||||
@@ -420,22 +278,7 @@ Track::getSimilarTracks(Session& session,
|
|||||||
for (TrackId trackId : tracks)
|
for (TrackId trackId : tracks)
|
||||||
query.bind(trackId);
|
query.bind(trackId);
|
||||||
|
|
||||||
auto res {query.resultList()};
|
return execQuery(query, range);
|
||||||
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
|
void
|
||||||
@@ -458,12 +301,6 @@ Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
|
|||||||
_clusters.insert(getDboPtr(cluster));
|
_clusters.insert(getDboPtr(cluster));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
Track::setFeatures(const ObjectPtr<TrackFeatures>& features)
|
|
||||||
{
|
|
||||||
_trackFeatures = getDboPtr(features);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::size_t>
|
std::optional<std::size_t>
|
||||||
Track::getTrackNumber() const
|
Track::getTrackNumber() const
|
||||||
{
|
{
|
||||||
@@ -593,12 +430,6 @@ Track::getArtistLinks() const
|
|||||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectPtr<TrackFeatures>
|
|
||||||
Track::getTrackFeatures() const
|
|
||||||
{
|
|
||||||
return _trackFeatures.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>>
|
std::vector<std::vector<Cluster::pointer>>
|
||||||
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
|
|
||||||
#include "Traits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Arti
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnumSet<TrackArtistLinkType>
|
EnumSet<TrackArtistLinkType>
|
||||||
TrackArtistLink::getUsedTypes(Session& session)
|
TrackArtistLink::findUsedTypes(Session& session)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "Traits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -32,6 +33,15 @@ _track {getDboPtr(track)}
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
TrackBookmark::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TrackBookmark::pointer
|
TrackBookmark::pointer
|
||||||
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||||
{
|
{
|
||||||
@@ -43,40 +53,30 @@ TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> t
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TrackBookmark::pointer>
|
RangeResults<TrackBookmarkId>
|
||||||
TrackBookmark::getAll(Session& session)
|
TrackBookmark::find(Session& session, UserId userId, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().find<TrackBookmark>().resultList()};
|
auto query {session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
|
||||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
.where("user_id = ?").bind(userId)};
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<TrackBookmark::pointer>
|
return execQuery(query, range);
|
||||||
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::pointer
|
||||||
TrackBookmark::getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<TrackBookmark>()
|
return session.getDboSession().find<TrackBookmark>()
|
||||||
.where("user_id = ?").bind(user->getId())
|
.where("user_id = ?").bind(userId)
|
||||||
.where("track_id = ?").bind(track->getId())
|
.where("track_id = ?").bind(trackId)
|
||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackBookmark::pointer
|
TrackBookmark::pointer
|
||||||
TrackBookmark::getById(Session& session, TrackBookmarkId id)
|
TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -85,6 +85,5 @@ TrackBookmark::getById(Session& session, TrackBookmarkId id)
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -34,11 +36,53 @@ _track {getDboPtr(track)}
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
TrackFeatures::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features");
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackFeatures::pointer
|
||||||
|
TrackFeatures::find(Session& session, TrackFeaturesId id)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().find<TrackFeatures>()
|
||||||
|
.where("id = ?").bind(id)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackFeatures::pointer
|
||||||
|
TrackFeatures::find(Session& session, TrackId trackId)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().find<TrackFeatures>()
|
||||||
|
.where("track_id = ?").bind(trackId)
|
||||||
|
.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackFeaturesId>
|
||||||
|
TrackFeatures::find(Session& session, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
TrackFeatures::pointer
|
TrackFeatures::pointer
|
||||||
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||||
{
|
{
|
||||||
session.checkUniqueLocked();
|
session.checkUniqueLocked();
|
||||||
return session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures));
|
|
||||||
|
TrackFeatures::pointer res {session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures))};
|
||||||
|
session.getDboSession().flush();
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
FeatureValues
|
FeatureValues
|
||||||
@@ -51,6 +95,8 @@ TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
|
|||||||
FeatureValuesMap
|
FeatureValuesMap
|
||||||
TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
||||||
{
|
{
|
||||||
|
FeatureValuesMap res;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::istringstream iss {_data};
|
std::istringstream iss {_data};
|
||||||
@@ -58,7 +104,6 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featur
|
|||||||
|
|
||||||
boost::property_tree::read_json(iss, root);
|
boost::property_tree::read_json(iss, root);
|
||||||
|
|
||||||
FeatureValuesMap res;
|
|
||||||
for (const FeatureName& featureName : featureNames)
|
for (const FeatureName& featureName : featureNames)
|
||||||
{
|
{
|
||||||
FeatureValues& featureValues {res[featureName]};
|
FeatureValues& featureValues {res[featureName]};
|
||||||
@@ -75,14 +120,14 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featur
|
|||||||
if (!hasChildren)
|
if (!hasChildren)
|
||||||
featureValues.push_back(node.get_value<double>());
|
featureValues.push_back(node.get_value<double>());
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
catch (boost::property_tree::ptree_error& error)
|
catch (boost::property_tree::ptree_error& error)
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
|
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
|
||||||
return {};
|
res.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "StringViewTraits.hpp"
|
#include "StringViewTraits.hpp"
|
||||||
#include "Traits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -55,56 +56,54 @@ TrackList::create(Session& session, std::string_view name, Type type, bool isPub
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer
|
std::size_t
|
||||||
TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr<User> user)
|
TrackList::getCount(Session& session)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
assert(user);
|
|
||||||
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TrackList::pointer
|
||||||
|
TrackList::find(Session& session, std::string_view name, Type type, UserId userId)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
assert(userId.isValid());
|
||||||
|
|
||||||
return session.getDboSession().find<TrackList>()
|
return session.getDboSession().find<TrackList>()
|
||||||
.where("name = ?").bind(name)
|
.where("name = ?").bind(name)
|
||||||
.where("type = ?").bind(type)
|
.where("type = ?").bind(type)
|
||||||
.where("user_id = ?").bind(user->getId()).resultValue();
|
.where("user_id = ?").bind(userId).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TrackList::pointer>
|
RangeResults<TrackListId>
|
||||||
TrackList::getAll(Session& session)
|
TrackList::find(Session& session, UserId userId, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res = session.getDboSession().find<TrackList>().resultList();
|
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist")
|
||||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
.where("user_id = ?").bind(userId)
|
||||||
|
.orderBy("name COLLATE NOCASE")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TrackList::pointer>
|
RangeResults<TrackListId>
|
||||||
TrackList::getAll(Session& session, ObjectPtr<User> user)
|
TrackList::find(Session& session, UserId userId, Type type, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
auto res {session.getDboSession().find<TrackList>()
|
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist")
|
||||||
.where("user_id = ?").bind(user->getId())
|
.where("user_id = ?").bind(userId)
|
||||||
.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)
|
.where("type = ?").bind(type)
|
||||||
.orderBy("name COLLATE NOCASE")
|
.orderBy("name COLLATE NOCASE")};
|
||||||
.resultList()};
|
|
||||||
|
|
||||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
return execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer
|
TrackList::pointer
|
||||||
TrackList::getById(Session& session, TrackListId id)
|
TrackList::find(Session& session, TrackListId id)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTim
|
|||||||
return session()->find<TrackListEntry>()
|
return session()->find<TrackListEntry>()
|
||||||
.where("tracklist_id = ?").bind(getId())
|
.where("tracklist_id = ?").bind(getId())
|
||||||
.where("track_id = ?").bind(track->getId())
|
.where("track_id = ?").bind(track->getId())
|
||||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
.where("date_time = ?").bind(normalizeDateTime(dateTime))
|
||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,19 +269,109 @@ createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std:
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<Artist::pointer>
|
||||||
TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
TrackList::getArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
|
||||||
|
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||||
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
|
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||||
|
|
||||||
|
switch (sortMethod)
|
||||||
|
{
|
||||||
|
case ArtistSortMethod::None:
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::ByName:
|
||||||
|
query.orderBy("a.name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::BySortName:
|
||||||
|
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::Random:
|
||||||
|
query.orderBy("RANDOM()");
|
||||||
|
break;
|
||||||
|
case ArtistSortMethod::LastWritten:
|
||||||
|
case ArtistSortMethod::StarredDateDesc:
|
||||||
|
assert(false); // Not implemented!
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection {query.resultList()};
|
||||||
|
|
||||||
|
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||||
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<ObjectPtr<Release>>
|
||||||
|
TrackList::getReleases(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)")
|
||||||
|
.limit(range ? static_cast<int>(range->size) + 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->size) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ObjectPtr<Track>>
|
||||||
|
TrackList::getTracks(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)")
|
||||||
|
.limit(range ? static_cast<int>(range->size) + 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->size) + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
res.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Artist::pointer>
|
||||||
|
TrackList::getArtistsOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
|
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)")
|
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||||
.orderBy("p_e.date_time DESC")
|
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -294,19 +383,19 @@ TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Release::pointer>
|
std::vector<Release::pointer>
|
||||||
TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
TrackList::getReleasesOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
|
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
|
||||||
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
|
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||||
.orderBy("p_e.date_time DESC")
|
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -318,19 +407,19 @@ TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer>
|
||||||
TrackList::getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
TrackList::getTracksOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
auto collection {createTracksQuery(*session(), getId(), clusterIds)
|
auto collection {createTracksQuery(*session(), getId(), clusterIds)
|
||||||
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
|
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||||
.orderBy("p_e.date_time DESC")
|
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -424,13 +513,13 @@ TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional
|
|||||||
auto collection {query
|
auto collection {query
|
||||||
.orderBy("COUNT(a.id) DESC")
|
.orderBy("COUNT(a.id) DESC")
|
||||||
.groupBy("a.id")
|
.groupBy("a.id")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
std::vector<Artist::pointer> res(collection.begin(), collection.end());
|
std::vector<Artist::pointer> res(collection.begin(), collection.end());
|
||||||
|
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -450,12 +539,12 @@ TrackList::getTopReleases(const std::vector<ClusterId>& clusterIds, std::optiona
|
|||||||
auto collection {query
|
auto collection {query
|
||||||
.orderBy("COUNT(r.id) DESC")
|
.orderBy("COUNT(r.id) DESC")
|
||||||
.groupBy("r.id")
|
.groupBy("r.id")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -475,12 +564,12 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
|
|||||||
auto collection {query
|
auto collection {query
|
||||||
.orderBy("COUNT(t.id) DESC")
|
.orderBy("COUNT(t.id) DESC")
|
||||||
.groupBy("t.id")
|
.groupBy("t.id")
|
||||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||||
.resultList()};
|
.resultList()};
|
||||||
|
|
||||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||||
{
|
{
|
||||||
moreResults = true;
|
moreResults = true;
|
||||||
res.pop_back();
|
res.pop_back();
|
||||||
@@ -492,11 +581,10 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
|
|||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||||
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution
|
: _dateTime {normalizeDateTime(dateTime)}
|
||||||
, _track {getDboPtr(track)}
|
, _track {getDboPtr(track)}
|
||||||
, _tracklist {getDboPtr(tracklist)}
|
, _tracklist {getDboPtr(tracklist)}
|
||||||
{
|
{
|
||||||
assert(_dateTime.isValid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::pointer
|
TrackListEntry::pointer
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
static const std::set<Bitrate> allowedAudioBitrates
|
||||||
|
{
|
||||||
|
64000,
|
||||||
|
96000,
|
||||||
|
128000,
|
||||||
|
192000,
|
||||||
|
320000,
|
||||||
|
};
|
||||||
|
|
||||||
|
void visitAllowedAudioBitrates(std::function<void(Bitrate)> func)
|
||||||
|
{
|
||||||
|
for (Bitrate bitrate : allowedAudioBitrates)
|
||||||
|
func(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAudioBitrateAllowed(Bitrate bitrate)
|
||||||
|
{
|
||||||
|
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateRange
|
||||||
|
DateRange::fromYearRange(int from, int to)
|
||||||
|
{
|
||||||
|
return DateRange {{from, 1, 1}, {to, 12, 31}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,85 +23,18 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
#include "IdTypeTraits.hpp"
|
||||||
#include "StringViewTraits.hpp"
|
#include "StringViewTraits.hpp"
|
||||||
#include "Traits.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
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)
|
User::User(std::string_view loginName)
|
||||||
: _loginName {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
|
std::size_t
|
||||||
User::getCount(Session& session)
|
User::getCount(Session& session)
|
||||||
{
|
{
|
||||||
@@ -110,28 +43,43 @@ User::getCount(Session& session)
|
|||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RangeResults<UserId>
|
||||||
|
User::find(Session& session, Range range)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto query {session.getDboSession().query<UserId>("SELECT id FROM user")};
|
||||||
|
|
||||||
|
return execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
User::pointer
|
||||||
|
User::findDemoUser(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer
|
||||||
User::create(Session& session, std::string_view loginName)
|
User::create(Session& session, std::string_view loginName)
|
||||||
{
|
{
|
||||||
session.checkUniqueLocked();
|
session.checkUniqueLocked();
|
||||||
|
|
||||||
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
|
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
|
||||||
|
|
||||||
TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user);
|
|
||||||
|
|
||||||
session.getDboSession().flush();
|
session.getDboSession().flush();
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer
|
||||||
User::getById(Session& session, UserId id)
|
User::find(Session& session, UserId id)
|
||||||
{
|
{
|
||||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
User::pointer
|
User::pointer
|
||||||
User::getByLoginName(Session& session, std::string_view name)
|
User::find(Session& session, std::string_view name)
|
||||||
{
|
{
|
||||||
return session.getDboSession().find<User>()
|
return session.getDboSession().find<User>()
|
||||||
.where("login_name = ?").bind(name)
|
.where("login_name = ?").bind(name)
|
||||||
@@ -141,7 +89,7 @@ User::getByLoginName(Session& session, std::string_view name)
|
|||||||
void
|
void
|
||||||
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
|
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
|
||||||
{
|
{
|
||||||
assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend());
|
assert(isAudioBitrateAllowed(bitrate));
|
||||||
_subsonicTranscodeBitrate = bitrate;
|
_subsonicTranscodeBitrate = bitrate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,75 +99,6 @@ User::clearAuthTokens()
|
|||||||
_authTokens.clear();
|
_authTokens.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer
|
|
||||||
User::getQueuedTrackList(Session& session) const
|
|
||||||
{
|
|
||||||
assert(self());
|
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
return TrackList::get(session, queuedListName, TrackList::Type::Internal, self());
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::star(ObjectPtr<Artist> artist)
|
|
||||||
{
|
|
||||||
if (_starredArtists.count(getDboPtr(artist)) == 0)
|
|
||||||
_starredArtists.insert(getDboPtr(artist));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::unstar(ObjectPtr<Artist> artist)
|
|
||||||
{
|
|
||||||
if (_starredArtists.count(getDboPtr(artist)) != 0)
|
|
||||||
_starredArtists.erase(getDboPtr(artist));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
User::isStarred(ObjectPtr<Artist> artist) const
|
|
||||||
{
|
|
||||||
return _starredArtists.count(getDboPtr(artist)) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::star(ObjectPtr<Release> release)
|
|
||||||
{
|
|
||||||
if (_starredReleases.count(getDboPtr(release)) == 0)
|
|
||||||
_starredReleases.insert(getDboPtr(release));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::unstar(ObjectPtr<Release> release)
|
|
||||||
{
|
|
||||||
if (_starredReleases.count(getDboPtr(release)) != 0)
|
|
||||||
_starredReleases.erase(getDboPtr(release));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
User::isStarred(ObjectPtr<Release> release) const
|
|
||||||
{
|
|
||||||
return _starredReleases.count(getDboPtr(release)) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::star(ObjectPtr<Track> track)
|
|
||||||
{
|
|
||||||
if (_starredTracks.count(getDboPtr(track)) == 0)
|
|
||||||
_starredTracks.insert(getDboPtr(track));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
User::unstar(ObjectPtr<Track> track)
|
|
||||||
{
|
|
||||||
if (_starredTracks.count(getDboPtr(track)) != 0)
|
|
||||||
_starredTracks.erase(getDboPtr(track));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
User::isStarred(ObjectPtr<Track> track) const
|
|
||||||
{
|
|
||||||
return _starredTracks.count(getDboPtr(track)) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,5 +29,12 @@ namespace Database
|
|||||||
return StringUtils::escapeString(keyword, "%_", escapeChar);
|
return StringUtils::escapeString(keyword, "%_", escapeChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime
|
||||||
|
normalizeDateTime(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
// force second resolution
|
||||||
|
return Wt::WDateTime::fromTime_t(dateTime.toTime_t());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,66 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
#define ESCAPE_CHAR_STR "\\"
|
#define ESCAPE_CHAR_STR "\\"
|
||||||
static constexpr char escapeChar {'\\'};
|
static inline constexpr char escapeChar {'\\'};
|
||||||
std::string escapeLikeKeyword(std::string_view keywords);
|
std::string escapeLikeKeyword(std::string_view keywords);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
RangeResults<T>
|
||||||
|
execQuery(Wt::Dbo::Query<T>& query, Range range)
|
||||||
|
{
|
||||||
|
RangeResults<T> res;
|
||||||
|
|
||||||
|
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1)
|
||||||
|
.offset(range.offset ? static_cast<int>(range.offset) : -1)
|
||||||
|
.resultList()};
|
||||||
|
|
||||||
|
res.results.assign(collection.begin(), collection.end());
|
||||||
|
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
||||||
|
{
|
||||||
|
res.moreResults = true;
|
||||||
|
res.results.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
res.moreResults = false;
|
||||||
|
|
||||||
|
res.range.offset = range.offset;
|
||||||
|
res.range.size = res.results.size();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
RangeResults<typename T::pointer>
|
||||||
|
execQuery(Wt::Dbo::Query<Wt::Dbo::ptr<T>>& query, Range range)
|
||||||
|
{
|
||||||
|
RangeResults<typename T::pointer> res;
|
||||||
|
|
||||||
|
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1)
|
||||||
|
.offset(range.offset ? static_cast<int>(range.offset) : -1)
|
||||||
|
.resultList()};
|
||||||
|
|
||||||
|
res.results.assign(collection.begin(), collection.end());
|
||||||
|
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
||||||
|
{
|
||||||
|
res.moreResults = true;
|
||||||
|
res.results.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
res.moreResults = false;
|
||||||
|
|
||||||
|
res.range.offset = range.offset;
|
||||||
|
res.range.size = res.results.size();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@
|
|||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@ class Cluster;
|
|||||||
class ClusterType;
|
class ClusterType;
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
|
class StarredArtist;
|
||||||
class Track;
|
class Track;
|
||||||
class TrackArtistLink;
|
class TrackArtistLink;
|
||||||
class User;
|
class User;
|
||||||
@@ -45,52 +50,38 @@ class User;
|
|||||||
class Artist : public Object<Artist, ArtistId>
|
class Artist : public Object<Artist, ArtistId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class SortMethod
|
struct FindParameters
|
||||||
{
|
{
|
||||||
None,
|
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||||
ByName,
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
|
||||||
BySortName,
|
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||||
|
ArtistSortMethod sortMethod {ArtistSortMethod::None};
|
||||||
|
Range range;
|
||||||
|
Wt::WDateTime writtenAfter;
|
||||||
|
UserId starringUser; // only artists starred by this user
|
||||||
|
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||||
|
|
||||||
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||||
|
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||||
|
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||||
|
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||||
|
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||||
|
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||||
};
|
};
|
||||||
|
|
||||||
Artist() = default;
|
Artist() = default;
|
||||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer getById(Session& session, ArtistId id);
|
static pointer find(Session& session, const UUID& MBID);
|
||||||
|
static pointer find(Session& session, ArtistId id);
|
||||||
|
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
|
||||||
|
static RangeResults<ArtistId> find(Session& session, const FindParameters& parameters);
|
||||||
|
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
|
||||||
static bool exists(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
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
@@ -101,11 +92,11 @@ class Artist : public Object<Artist, ArtistId>
|
|||||||
std::size_t getReleaseCount() const;
|
std::size_t getReleaseCount() const;
|
||||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) 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;
|
RangeResults<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const;
|
||||||
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||||
|
|
||||||
// No artistLinkTypes means get them all
|
// No artistLinkTypes means get them all
|
||||||
std::vector<pointer> getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
|
||||||
|
|
||||||
// Get the cluster of the tracks made by this artist
|
// Get the cluster of the tracks made by this artist
|
||||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||||
@@ -127,7 +118,7 @@ class Artist : public Object<Artist, ArtistId>
|
|||||||
Wt::Dbo::field(a, _MBID, "mbid");
|
Wt::Dbo::field(a, _MBID, "mbid");
|
||||||
|
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||||
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -139,7 +130,7 @@ class Artist : public Object<Artist, ArtistId>
|
|||||||
std::string _MBID; // Musicbrainz Identifier
|
std::string _MBID; // Musicbrainz Identifier
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
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
|
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ArtistId)
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/AuthTokenId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
|
||||||
|
class User;
|
||||||
|
class AuthToken : public Object<AuthToken, AuthTokenId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AuthToken() = default;
|
||||||
|
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||||
|
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||||
|
static pointer find(Session& session, std::string_view value);
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||||
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
const std::string& getValue() const { return _value; }
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _value, "value");
|
||||||
|
Wt::Dbo::field(a, _expiry, "expiry");
|
||||||
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::string _value;
|
||||||
|
Wt::WDateTime _expiry;
|
||||||
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
};
|
||||||
|
} // namespace Databas'
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(AuthTokenId)
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
@@ -42,9 +45,10 @@ class Cluster : public Object<Cluster, ClusterId>
|
|||||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
|
|
||||||
// Find utility
|
// Find utility
|
||||||
static std::vector<pointer> getAll(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static std::vector<pointer> getAllOrphans(Session& session);
|
static RangeResults<ClusterId> find(Session& session, Range range);
|
||||||
static pointer getById(Session& session, ClusterId id);
|
static pointer find(Session& session, ClusterId id);
|
||||||
|
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||||
|
|
||||||
// Create utility
|
// Create utility
|
||||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
@@ -53,8 +57,7 @@ class Cluster : public Object<Cluster, ClusterId>
|
|||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||||
std::size_t getTracksCount() const { return _tracks.size(); }
|
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;
|
RangeResults<TrackId> getTracks(Range range) const;
|
||||||
std::vector<TrackId> getTrackIds() const;
|
|
||||||
std::size_t getReleasesCount() const;
|
std::size_t getReleasesCount() const;
|
||||||
|
|
||||||
void addTrack(ObjectPtr<Track> track);
|
void addTrack(ObjectPtr<Track> track);
|
||||||
@@ -85,17 +88,18 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
|
|||||||
ClusterType(std::string_view name);
|
ClusterType(std::string_view name);
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
static std::vector<pointer> getAllOrphans(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static std::vector<pointer> getAllUsed(Session& session);
|
static RangeResults<ClusterTypeId> find(Session& session, Range range);
|
||||||
static pointer getByName(Session& session, const std::string& name);
|
static pointer find(Session& session, const std::string& name);
|
||||||
static pointer getById(Session& session, ClusterTypeId id);
|
static pointer find(Session& session, ClusterTypeId id);
|
||||||
static std::vector<pointer> getAll(Session& session);
|
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
||||||
|
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
||||||
|
|
||||||
static pointer create(Session& session, const std::string& name);
|
static pointer create(Session& session, const std::string& name);
|
||||||
static void remove(Session& session, const std::string& name);
|
static void remove(Session& session, const std::string& name);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName(void) const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
std::vector<Cluster::pointer> getClusters() const;
|
std::vector<Cluster::pointer> getClusters() const;
|
||||||
Cluster::pointer getCluster(const std::string& name) const;
|
Cluster::pointer getCluster(const std::string& name) const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ClusterId)
|
||||||
|
LMS_DECLARE_IDTYPE(ClusterTypeId)
|
||||||
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <functional>
|
||||||
|
#include <Wt/Dbo/ptr.h>
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class IdType
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||||
|
|
||||||
|
IdType() = default;
|
||||||
|
IdType(ValueType id) : _id {id} { assert(isValid()); }
|
||||||
|
|
||||||
|
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
|
||||||
|
std::string toString() const { assert(isValid()); return std::to_string(_id); }
|
||||||
|
|
||||||
|
ValueType getValue() const { return _id; }
|
||||||
|
|
||||||
|
bool operator==(IdType other) const { return other._id == _id; }
|
||||||
|
bool operator!=(IdType other) const { return !(*this == other); }
|
||||||
|
bool operator<(IdType other) const { return _id < other._id; }
|
||||||
|
bool operator>(IdType other) const { return _id > other._id; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
|
||||||
|
};
|
||||||
|
|
||||||
|
#define LMS_DECLARE_IDTYPE(name) \
|
||||||
|
namespace Database { \
|
||||||
|
class name : public IdType \
|
||||||
|
{ \
|
||||||
|
public: \
|
||||||
|
using IdType::IdType; \
|
||||||
|
};\
|
||||||
|
} \
|
||||||
|
namespace std \
|
||||||
|
{ \
|
||||||
|
template<> \
|
||||||
|
class hash<Database::name> \
|
||||||
|
{ \
|
||||||
|
public: \
|
||||||
|
size_t operator()(Database::name id) const \
|
||||||
|
{ \
|
||||||
|
return std::hash<Database::name::ValueType>()(id.getValue()); \
|
||||||
|
} \
|
||||||
|
}; \
|
||||||
|
} // ns std
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ListenId)
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
|
||||||
|
class Session;
|
||||||
|
class Track;
|
||||||
|
class User;
|
||||||
|
|
||||||
|
class Listen : public Object<Listen, ListenId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Listen() = default;
|
||||||
|
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, ListenId id);
|
||||||
|
static RangeResults<pointer> find(Session& session, UserId userId, Scrobbler scrobbler, Range = {});
|
||||||
|
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
// Create
|
||||||
|
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
static RangeResults<ArtistId> getTopArtists(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
|
Range range = {});
|
||||||
|
static RangeResults<ReleaseId> getTopReleases(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range = {});
|
||||||
|
static RangeResults<TrackId> getTopTracks(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range = {});
|
||||||
|
|
||||||
|
static RangeResults<ArtistId> getRecentArtists(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
|
Range range = {});
|
||||||
|
static RangeResults<ReleaseId> getRecentReleases(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range = {});
|
||||||
|
static RangeResults<TrackId> getRecentTracks(Session& session,
|
||||||
|
UserId userId,
|
||||||
|
Scrobbler scrobbler,
|
||||||
|
const std::vector<ClusterId>& clusterIds,
|
||||||
|
Range range = {});
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Wt::WDateTime _dateTime;
|
||||||
|
Scrobbler _scrobbler;
|
||||||
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
Wt::Dbo::ptr<Track> _track;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/Dbo/ptr.h>
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
class ObjectPtr
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ObjectPtr() = default;
|
||||||
|
ObjectPtr(Wt::Dbo::ptr<T> obj) : _obj {obj} {}
|
||||||
|
|
||||||
|
const T* operator->() const { return _obj.get(); }
|
||||||
|
operator bool() const { return _obj.get(); }
|
||||||
|
bool operator!() const { return !_obj.get(); }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,7 +25,11 @@
|
|||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
@@ -42,32 +46,38 @@ class User;
|
|||||||
class Release : public Object<Release, ReleaseId>
|
class Release : public Object<Release, ReleaseId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct FindParameters
|
||||||
|
{
|
||||||
|
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
||||||
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
|
ReleaseSortMethod sortMethod {ReleaseSortMethod::None};
|
||||||
|
Range range;
|
||||||
|
Wt::WDateTime writtenAfter;
|
||||||
|
std::optional<DateRange> dateRange;
|
||||||
|
UserId starringUser; // only releases starred by this user
|
||||||
|
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||||
|
|
||||||
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||||
|
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||||
|
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||||
|
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; }
|
||||||
|
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) {dateRange = _dateRange; return *this; }
|
||||||
|
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||||
|
};
|
||||||
|
|
||||||
Release() = default;
|
Release() = default;
|
||||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
static std::size_t getCount(Session& session);
|
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 bool exists(Session& session, ReleaseId id);
|
||||||
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
static pointer find(Session& session, const UUID& MBID);
|
||||||
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||||
static std::vector<ReleaseId> getAllIds(Session& session);
|
static pointer find(Session& session, ReleaseId id);
|
||||||
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
||||||
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
|
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||||
static std::vector<ReleaseId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> size = {});
|
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
||||||
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::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters = {}) const;
|
||||||
std::size_t getTracksCount() const;
|
std::size_t getTracksCount() const;
|
||||||
@@ -110,7 +120,6 @@ class Release : public Object<Release, ReleaseId>
|
|||||||
Wt::Dbo::field(a, _MBID, "mbid");
|
Wt::Dbo::field(a, _MBID, "mbid");
|
||||||
|
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||||
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -120,7 +129,6 @@ class Release : public Object<Release, ReleaseId>
|
|||||||
std::string _MBID;
|
std::string _MBID;
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
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
|
} // namespace Database
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ReleaseId)
|
||||||
|
|
||||||
@@ -25,7 +25,10 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WTime.h>
|
#include <Wt/WTime.h>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(StarredArtistId)
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Artist;
|
||||||
|
class Session;
|
||||||
|
class User;
|
||||||
|
|
||||||
|
class StarredArtist : public Object<StarredArtist, StarredArtistId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StarredArtist() = default;
|
||||||
|
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Search utility
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, StarredArtistId id);
|
||||||
|
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Create utility
|
||||||
|
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||||
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||||
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Scrobbler _scrobbler; // for which scrobbler
|
||||||
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<Artist> _artist;
|
||||||
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
};
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(StarredReleaseId)
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Release;
|
||||||
|
class Session;
|
||||||
|
class User;
|
||||||
|
|
||||||
|
class StarredRelease : public Object<StarredRelease, StarredReleaseId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StarredRelease() = default;
|
||||||
|
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Search utility
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, StarredReleaseId id);
|
||||||
|
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Create utility
|
||||||
|
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
ObjectPtr<Release> getRelease() const { return _release; }
|
||||||
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||||
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Scrobbler _scrobbler; // for which scrobbler
|
||||||
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<Release> _release;
|
||||||
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
};
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(StarredTrackId)
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Track;
|
||||||
|
class Session;
|
||||||
|
class User;
|
||||||
|
|
||||||
|
class StarredTrack : public Object<StarredTrack, StarredTrackId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StarredTrack() = default;
|
||||||
|
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Search utility
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, StarredTrackId id);
|
||||||
|
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Create utility
|
||||||
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||||
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Scrobbler _scrobbler; // for which scrobbler
|
||||||
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<Track> _track;
|
||||||
|
Wt::Dbo::ptr<User> _user;
|
||||||
|
};
|
||||||
|
} // namespace Database
|
||||||
|
|
||||||
@@ -34,7 +34,12 @@
|
|||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -44,50 +49,51 @@ class ClusterType;
|
|||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class TrackArtistLink;
|
class TrackArtistLink;
|
||||||
class TrackFeatures;
|
|
||||||
class TrackListEntry;
|
|
||||||
class TrackStats;
|
class TrackStats;
|
||||||
class User;
|
class User;
|
||||||
|
|
||||||
class Track : public Object<Track, TrackId>
|
class Track : public Object<Track, TrackId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct FindParameters
|
||||||
|
{
|
||||||
|
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
||||||
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
|
TrackSortMethod sortMethod {TrackSortMethod::None};
|
||||||
|
Range range;
|
||||||
|
Wt::WDateTime writtenAfter;
|
||||||
|
UserId starringUser; // only tracks starred by this user
|
||||||
|
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||||
|
|
||||||
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||||
|
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
|
||||||
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
|
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||||
|
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||||
|
};
|
||||||
|
struct PathResult
|
||||||
|
{
|
||||||
|
TrackId trackId;
|
||||||
|
std::filesystem::path path;
|
||||||
|
};
|
||||||
|
|
||||||
Track() = default;
|
Track() = default;
|
||||||
Track(const std::filesystem::path& p);
|
Track(const std::filesystem::path& p);
|
||||||
|
|
||||||
// Find utility functions
|
// Find utility functions
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
static pointer findByPath(Session& session, const std::filesystem::path& p);
|
||||||
static pointer getById(Session& session, TrackId id);
|
static pointer find(Session& session, TrackId id);
|
||||||
static bool exists(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> findByRecordingMBID(Session& session, const UUID& MBID);
|
||||||
static std::vector<pointer> getSimilarTracks(Session& session,
|
static RangeResults<TrackId> findSimilarTracks(Session& session, const std::vector<TrackId>& trackIds, Range range);
|
||||||
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 RangeResults<TrackId> find(Session& session, const FindParameters& parameters);
|
||||||
static std::vector<pointer> getAllRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
static RangeResults<TrackId> findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName);
|
||||||
static std::vector<TrackId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
static RangeResults<PathResult> findPaths(Session& session, Range range);
|
||||||
static std::vector<TrackId> getAllIds(Session& session);
|
static RangeResults<TrackId> findMBIDDuplicates(Session& session, Range range);
|
||||||
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 RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(Session& session, Range range);
|
||||||
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
|
// Create utility
|
||||||
static pointer create(Session& session, const std::filesystem::path& p);
|
static pointer create(Session& session, const std::filesystem::path& p);
|
||||||
@@ -116,7 +122,6 @@ class Track : public Object<Track, TrackId>
|
|||||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters );
|
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters );
|
||||||
void setFeatures(const ObjectPtr<TrackFeatures>& features);
|
|
||||||
|
|
||||||
std::size_t getScanVersion() const { return _scanVersion; }
|
std::size_t getScanVersion() const { return _scanVersion; }
|
||||||
std::optional<std::size_t> getTrackNumber() const;
|
std::optional<std::size_t> getTrackNumber() const;
|
||||||
@@ -147,8 +152,6 @@ class Track : public Object<Track, TrackId>
|
|||||||
ObjectPtr<Release> getRelease() const { return _release; }
|
ObjectPtr<Release> getRelease() const { return _release; }
|
||||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||||
std::vector<ClusterId> getClusterIds() 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;
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||||
|
|
||||||
@@ -178,9 +181,6 @@ class Track : public Object<Track, TrackId>
|
|||||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
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, _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:
|
private:
|
||||||
@@ -215,10 +215,6 @@ class Track : public Object<Track, TrackId>
|
|||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
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
|
} // namespace database
|
||||||
|
|||||||
@@ -23,9 +23,13 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -40,8 +44,7 @@ namespace Database
|
|||||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||||
|
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||||
|
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||||
static EnumSet<TrackArtistLinkType> getUsedTypes(Session& session);
|
|
||||||
|
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||||
|
|||||||
@@ -23,7 +23,13 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(TrackBookmarkId)
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -41,10 +47,10 @@ class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
|
|||||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||||
|
|
||||||
// Find utility functions
|
// Find utility functions
|
||||||
static std::vector<pointer> getAll(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static std::vector<pointer> getByUser(Session& session, ObjectPtr<User> user);
|
static pointer find(Session& session, TrackBookmarkId id);
|
||||||
static pointer getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
static RangeResults<TrackBookmarkId> find(Session& session, UserId userId, Range range);
|
||||||
static pointer getById(Session& session, TrackBookmarkId id);
|
static pointer find(Session& session, UserId userId, TrackId trackId);
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
|
void setOffset(std::chrono::milliseconds offset) { _offset = offset; }
|
||||||
|
|||||||
@@ -26,8 +26,13 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
class Session;
|
class Session;
|
||||||
@@ -43,12 +48,21 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
|||||||
TrackFeatures() = default;
|
TrackFeatures() = default;
|
||||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||||
|
|
||||||
|
// Find utilities
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, TrackFeaturesId id);
|
||||||
|
static pointer find(Session& session, TrackId trackId);
|
||||||
|
static RangeResults<TrackFeaturesId> find(Session& session, Range range);
|
||||||
|
|
||||||
// Create utility
|
// Create utility
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||||
|
|
||||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
||||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
@@ -57,7 +71,6 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
std::string _data;
|
std::string _data;
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(TrackId)
|
||||||
|
|
||||||
@@ -21,13 +21,18 @@
|
|||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <set>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
|
#include "services/database/TrackListId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
@@ -57,17 +62,17 @@ class TrackList : public Object<TrackList, TrackListId>
|
|||||||
std::vector<ObjectPtr<Track>> getTopTracks(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
|
// Search utility
|
||||||
static pointer get(Session& session, std::string_view name, Type type, ObjectPtr<User> user);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer getById(Session& session, TrackListId tracklistId);
|
static pointer find(Session& session, std::string_view name, Type type, UserId userId);
|
||||||
static std::vector<pointer> getAll(Session& session);
|
static pointer find(Session& session, TrackListId tracklistId);
|
||||||
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user);
|
static RangeResults<TrackListId> find(Session& session, UserId userId, Range range);
|
||||||
static std::vector<pointer> getAll(Session& session, ObjectPtr<User> user, Type type);
|
static RangeResults<TrackListId> find(Session& session, UserId userId, Type type, Range range);
|
||||||
|
|
||||||
// Create utility
|
// Create utility
|
||||||
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
std::string getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
bool isPublic() const { return _isPublic; }
|
bool isPublic() const { return _isPublic; }
|
||||||
Type getType() const { return _type; }
|
Type getType() const { return _type; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
@@ -84,14 +89,16 @@ class TrackList : public Object<TrackList, TrackListId>
|
|||||||
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) 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;
|
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
|
||||||
|
|
||||||
// Get track bya
|
std::vector<ObjectPtr<Artist>> getArtists(const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
|
||||||
std::vector<ObjectPtr<Artist>> getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
// Sorted by date time
|
||||||
std::vector<ObjectPtr<Release>> getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
std::vector<ObjectPtr<Artist>> getArtistsOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, 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<ObjectPtr<Release>> getReleasesOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
std::vector<ObjectPtr<Track>> getTracksOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
|
||||||
std::vector<TrackId> getTrackIds() const;
|
std::vector<TrackId> getTrackIds() const;
|
||||||
|
|
||||||
std::chrono::milliseconds getDuration() const;
|
std::chrono::milliseconds getDuration() const;
|
||||||
|
|
||||||
// Get clusters, order by occurence
|
// Get clusters, order by occurence
|
||||||
@@ -129,12 +136,13 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
|
|||||||
public:
|
public:
|
||||||
TrackListEntry() = default;
|
TrackListEntry() = default;
|
||||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||||
|
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
|
||||||
|
|
||||||
// find utility
|
// find utility
|
||||||
static pointer getById(Session& session, TrackListEntryId id);
|
static pointer getById(Session& session, TrackListEntryId id);
|
||||||
|
|
||||||
// Create utility
|
// Create utility
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime());
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
@@ -151,7 +159,7 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Wt::WDateTime _dateTime;
|
Wt::WDateTime _dateTime; // optional date time
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
Wt::Dbo::ptr<TrackList> _tracklist;
|
Wt::Dbo::ptr<TrackList> _tracklist;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(TrackListId)
|
||||||
|
LMS_DECLARE_IDTYPE(TrackListEntryId)
|
||||||
|
|
||||||
@@ -22,50 +22,109 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <Wt/Dbo/ptr.h>
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class IdType
|
// Caution: do not change enum values if they are set!
|
||||||
{
|
|
||||||
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()};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Request:
|
||||||
|
// size = 0 => no size limit!
|
||||||
|
// Response (via RangeResults)
|
||||||
|
// size => results size
|
||||||
struct Range
|
struct Range
|
||||||
{
|
{
|
||||||
std::size_t offset {};
|
std::size_t offset {};
|
||||||
std::size_t limit {};
|
std::size_t size {};
|
||||||
|
|
||||||
|
// TODO remove this
|
||||||
|
operator bool() const { return size != 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct RangeResults
|
||||||
|
{
|
||||||
|
Range range;
|
||||||
|
std::vector<T> results;
|
||||||
|
bool moreResults;
|
||||||
|
|
||||||
|
RangeResults getSubRange(Range subRange)
|
||||||
|
{
|
||||||
|
assert(subRange.offset >= range.offset);
|
||||||
|
|
||||||
|
if (!subRange.size)
|
||||||
|
subRange.size = range.size - (subRange.offset - range.offset);
|
||||||
|
|
||||||
|
subRange.offset = std::min(subRange.offset, range.offset + range.size);
|
||||||
|
subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset);
|
||||||
|
|
||||||
|
RangeResults subResults;
|
||||||
|
|
||||||
|
auto itBegin {std::cbegin(results) + subRange.offset - range.offset};
|
||||||
|
auto itEnd {itBegin + subRange.size};
|
||||||
|
subResults.results.reserve(std::distance(itBegin, itEnd));
|
||||||
|
std::copy(itBegin, itEnd, std::back_inserter(subResults.results));
|
||||||
|
|
||||||
|
subResults.range = subRange;
|
||||||
|
if (subRange.offset + subRange.size == range.offset + range.size)
|
||||||
|
subResults.moreResults = moreResults;
|
||||||
|
else
|
||||||
|
subResults.moreResults = true;
|
||||||
|
|
||||||
|
return subResults;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DateRange
|
||||||
|
{
|
||||||
|
Wt::WDate begin;
|
||||||
|
Wt::WDate end;
|
||||||
|
|
||||||
|
static DateRange fromYearRange(int from, int to);
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ArtistSortMethod
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
ByName,
|
||||||
|
BySortName,
|
||||||
|
Random,
|
||||||
|
LastWritten,
|
||||||
|
StarredDateDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ReleaseSortMethod
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Name,
|
||||||
|
Date,
|
||||||
|
Random,
|
||||||
|
LastWritten,
|
||||||
|
StarredDateDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TrackSortMethod
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Random,
|
||||||
|
LastWritten,
|
||||||
|
StarredDateDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do not change enum values!
|
||||||
enum class TrackArtistLinkType
|
enum class TrackArtistLinkType
|
||||||
{
|
{
|
||||||
Artist, // regular artist
|
Artist = 0, // regular track artist
|
||||||
Arranger,
|
Arranger = 1,
|
||||||
Composer,
|
Composer = 2,
|
||||||
Conductor,
|
Conductor = 3,
|
||||||
Lyricist,
|
Lyricist = 4,
|
||||||
Mixer,
|
Mixer = 5,
|
||||||
Performer,
|
Performer = 6,
|
||||||
Producer,
|
Producer = 7,
|
||||||
ReleaseArtist,
|
ReleaseArtist = 8,
|
||||||
Remixer,
|
Remixer = 9,
|
||||||
Writer,
|
Writer = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// User selectable audio file formats
|
// User selectable audio file formats
|
||||||
@@ -78,7 +137,11 @@ namespace Database
|
|||||||
WEBM_VORBIS = 4,
|
WEBM_VORBIS = 4,
|
||||||
MATROSKA_OPUS = 5,
|
MATROSKA_OPUS = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
using Bitrate = std::uint32_t;
|
using Bitrate = std::uint32_t;
|
||||||
|
// Do not remove values!
|
||||||
|
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||||
|
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||||
|
|
||||||
// Do not change enum values!
|
// Do not change enum values!
|
||||||
enum class Scrobbler
|
enum class Scrobbler
|
||||||
@@ -95,82 +158,19 @@ namespace Database
|
|||||||
DEMO = 2,
|
DEMO = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T>
|
// Do not change enum values!
|
||||||
class ObjectPtr
|
enum class UITheme
|
||||||
{
|
{
|
||||||
public:
|
Light = 0,
|
||||||
ObjectPtr() = default;
|
Dark = 1,
|
||||||
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>
|
// Do not change enum values!
|
||||||
class Object : public Wt::Dbo::Dbo<T>
|
enum class SubsonicArtistListMode
|
||||||
{
|
{
|
||||||
static_assert(std::is_base_of_v<Database::IdType, ObjectIdType>);
|
AllArtists = 0,
|
||||||
static_assert(!std::is_same_v<Database::IdType, ObjectIdType>);
|
ReleaseArtists = 1,
|
||||||
|
TrackArtists = 2,
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,51 +26,15 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
|
class AuthToken;
|
||||||
class Artist;
|
|
||||||
class Release;
|
|
||||||
class Session;
|
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>
|
class User : public Object<User, UserId>
|
||||||
{
|
{
|
||||||
@@ -81,31 +45,6 @@ class User : public Object<User, UserId>
|
|||||||
std::string hash;
|
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 MinNameLength {3};
|
||||||
static inline const std::size_t MaxNameLength {15};
|
static inline const std::size_t MaxNameLength {15};
|
||||||
static inline const bool defaultSubsonicTranscodeEnable {true};
|
static inline const bool defaultSubsonicTranscodeEnable {true};
|
||||||
@@ -121,17 +60,16 @@ class User : public Object<User, UserId>
|
|||||||
// utility
|
// utility
|
||||||
static pointer create(Session& session, std::string_view loginName);
|
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);
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, UserId id);
|
||||||
|
static pointer find(Session& session, std::string_view loginName);
|
||||||
|
static RangeResults<UserId> find(Session& session, Range range);
|
||||||
|
static pointer findDemoUser(Session& session);
|
||||||
|
|
||||||
// accessors
|
// accessors
|
||||||
const std::string& getLoginName() const { return _loginName; }
|
const std::string& getLoginName() const { return _loginName; }
|
||||||
PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; }
|
PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; }
|
||||||
Wt::WDateTime getLastLogin() const { return _lastLogin; }
|
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
|
||||||
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
|
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
|
||||||
|
|
||||||
// write
|
// write
|
||||||
@@ -165,21 +103,6 @@ class User : public Object<User, UserId>
|
|||||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||||
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
||||||
|
|
||||||
ObjectPtr<TrackList> getQueuedTrackList(Session& session) const;
|
|
||||||
|
|
||||||
void star(ObjectPtr<Artist> artist);
|
|
||||||
void unstar(ObjectPtr<Artist> artist);
|
|
||||||
bool isStarred(ObjectPtr<Artist> artist) const;
|
|
||||||
|
|
||||||
void star(ObjectPtr<Release> release);
|
|
||||||
void unstar(ObjectPtr<Release> release);
|
|
||||||
bool isStarred(ObjectPtr<Release> release) const;
|
|
||||||
|
|
||||||
// Stars
|
|
||||||
void star(ObjectPtr<Track> track);
|
|
||||||
void unstar(ObjectPtr<Track> track);
|
|
||||||
bool isStarred(ObjectPtr<Track> track) const;
|
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
@@ -196,20 +119,15 @@ class User : public Object<User, UserId>
|
|||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||||
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
||||||
|
|
||||||
// UI settings
|
// UI player settings
|
||||||
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
||||||
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
||||||
Wt::Dbo::field(a, _radio, "radio");
|
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");
|
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
std::string _loginName;
|
std::string _loginName;
|
||||||
std::string _passwordSalt;
|
std::string _passwordSalt;
|
||||||
std::string _passwordHash;
|
std::string _passwordHash;
|
||||||
@@ -232,12 +150,7 @@ class User : public Object<User, UserId>
|
|||||||
bool _repeatAll {};
|
bool _repeatAll {};
|
||||||
bool _radio {};
|
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;
|
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Databas'
|
} // namespace Databas'
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(UserId)
|
||||||
|
|
||||||
@@ -21,13 +21,14 @@
|
|||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleArtist)
|
TEST_F(DatabaseFixture, Artist)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_FALSE(Artist::exists(session, 35));
|
EXPECT_FALSE(Artist::exists(session, 35));
|
||||||
EXPECT_FALSE(Artist::exists(session, 0));
|
EXPECT_FALSE(Artist::exists(session, 0));
|
||||||
EXPECT_FALSE(Artist::exists(session, 1));
|
EXPECT_FALSE(Artist::exists(session, 1));
|
||||||
|
EXPECT_EQ(Artist::getCount(session), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
@@ -40,22 +41,23 @@ TEST_F(DatabaseFixture, SingleArtist)
|
|||||||
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
EXPECT_EQ(artist.get()->getId(), artist.getId());
|
||||||
|
|
||||||
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
EXPECT_TRUE(Artist::exists(session, artist.getId()));
|
||||||
|
EXPECT_EQ(Artist::getCount(session), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getAll(session, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
artists = Artist::getAllOrphans(session);
|
artists = Artist::findAllOrphans(session, Range {});
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackSingleArtist)
|
TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
@@ -68,7 +70,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -102,7 +104,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles)
|
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
@@ -116,17 +118,16 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
bool hasMore{};
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1);
|
||||||
EXPECT_EQ(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
|
||||||
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
|
||||||
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
|
||||||
EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
|
||||||
EXPECT_TRUE(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -149,7 +150,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture,SingleTrackMultiArtists)
|
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "track"};
|
ScopedTrack track {session, "track"};
|
||||||
ScopedArtist artist1 {session, "artist1"};
|
ScopedArtist artist1 {session, "artist1"};
|
||||||
@@ -165,7 +166,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -179,8 +180,8 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists)
|
|||||||
EXPECT_EQ(track->getArtists({}).size(), 2);
|
EXPECT_EQ(track->getArtists({}).size(), 2);
|
||||||
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2);
|
EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2);
|
||||||
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
|
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
|
||||||
EXPECT_EQ(Artist::getAll(session, Artist::SortMethod::ByName).size(), 2);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2);
|
||||||
EXPECT_EQ(Artist::getAllIds(session).size(), 2);
|
EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -196,7 +197,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleArtistSearchByName)
|
TEST_F(DatabaseFixture, Artist_findByName)
|
||||||
{
|
{
|
||||||
ScopedArtist artist {session, "AAA"};
|
ScopedArtist artist {session, "AAA"};
|
||||||
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
|
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
|
||||||
@@ -210,22 +211,22 @@ TEST_F(DatabaseFixture, SingleArtistSearchByName)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool more {};
|
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty());
|
||||||
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)};
|
const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))};
|
||||||
ASSERT_EQ(artistsByAAA.size(), 1);
|
ASSERT_EQ(artistsByAAA.results.size(), 1);
|
||||||
EXPECT_EQ(artistsByAAA.front()->getId(), artist.getId());
|
EXPECT_EQ(artistsByAAA.results.front(), artist.getId());
|
||||||
|
|
||||||
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))};
|
||||||
ASSERT_EQ(artistsByZZZ.size(), 1);
|
ASSERT_EQ(artistsByZZZ.results.size(), 1);
|
||||||
EXPECT_EQ(artistsByZZZ.front()->getId(), artist.getId());
|
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId());
|
||||||
|
|
||||||
EXPECT_TRUE(Artist::getByName(session, "NNN").empty());
|
EXPECT_TRUE(Artist::find(session, "NNN").empty());
|
||||||
|
EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped)
|
TEST_F(DatabaseFixture, Artist_findByNameEscaped)
|
||||||
{
|
{
|
||||||
ScopedArtist artist1 {session, "MyArtist%"};
|
ScopedArtist artist1 {session, "MyArtist%"};
|
||||||
ScopedArtist artist2 {session, "%MyArtist"};
|
ScopedArtist artist2 {session, "%MyArtist"};
|
||||||
@@ -238,70 +239,55 @@ TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByName(session, "MyArtist%")};
|
const auto artists {Artist::find(session, "MyArtist%")};
|
||||||
ASSERT_TRUE(artists.size() == 1);
|
ASSERT_TRUE(artists.size() == 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
EXPECT_TRUE(Artist::getByName(session, "MyArtistFoo").empty());
|
EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByName(session, "%MyArtist")};
|
const auto artists {Artist::find(session, "%MyArtist")};
|
||||||
ASSERT_TRUE(artists.size() == 1);
|
ASSERT_TRUE(artists.size() == 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||||
EXPECT_TRUE(Artist::getByName(session, "FooMyArtist").empty());
|
EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByName(session, "%_MyArtist")};
|
const auto artists {Artist::find(session, "%_MyArtist")};
|
||||||
ASSERT_TRUE(artists.size() == 1);
|
ASSERT_TRUE(artists.size() == 1);
|
||||||
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
||||||
EXPECT_TRUE(Artist::getByName(session, "%CMyArtist").empty());
|
EXPECT_TRUE(Artist::find(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()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
bool more;
|
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByFilter(session, {}, {"MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))};
|
||||||
EXPECT_EQ(artists.size(), 6);
|
EXPECT_EQ(artists.results.size(), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByFilter(session, {}, {"MyArtist%"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist4.getId());
|
EXPECT_EQ(artists.results[1], artist4.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByFilter(session, {}, {"%MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist2.getId());
|
EXPECT_EQ(artists.results[0], artist2.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist5.getId());
|
EXPECT_EQ(artists.results[1], artist5.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto artists {Artist::getByFilter(session, {}, {"_MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist3.getId());
|
EXPECT_EQ(artists.results[0], artist3.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultiArtistsSortMethod)
|
TEST_F(DatabaseFixture, Artist_sortMethod)
|
||||||
{
|
{
|
||||||
ScopedArtist artistA {session, "artistA"};
|
ScopedArtist artistA {session, "artistA"};
|
||||||
ScopedArtist artistB {session, "artistB"};
|
ScopedArtist artistB {session, "artistB"};
|
||||||
@@ -316,20 +302,20 @@ TEST_F(DatabaseFixture, MultiArtistsSortMethod)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto allArtistsByName {Artist::getAll(session, Artist::SortMethod::ByName)};
|
auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))};
|
||||||
auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)};
|
auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
|
||||||
|
|
||||||
ASSERT_EQ(allArtistsByName.size(), 2);
|
ASSERT_EQ(allArtistsByName.results.size(), 2);
|
||||||
EXPECT_EQ(allArtistsByName.front()->getId(), artistA.getId());
|
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
|
||||||
EXPECT_EQ(allArtistsByName.back()->getId(), artistB.getId());
|
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId());
|
||||||
|
|
||||||
ASSERT_EQ(allArtistsBySortName.size(), 2);
|
ASSERT_EQ(allArtistsBySortName.results.size(), 2);
|
||||||
EXPECT_EQ(allArtistsBySortName.front()->getId(), artistB.getId());
|
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId());
|
||||||
EXPECT_EQ(allArtistsBySortName.back()->getId(), artistA.getId());
|
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks)
|
TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||||
{
|
{
|
||||||
ScopedArtist artist {session, "artist"};
|
ScopedArtist artist {session, "artist"};
|
||||||
ScopedTrack track1 {session, "MyTrack1"};
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
@@ -340,9 +326,8 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt));
|
EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt));
|
||||||
|
|
||||||
bool moreResults;
|
const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})};
|
||||||
const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )};
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
EXPECT_EQ(tracks.size(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -358,44 +343,11 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults;
|
const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})};
|
||||||
const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )};
|
|
||||||
EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt));
|
EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt));
|
||||||
EXPECT_FALSE(moreResults);
|
EXPECT_FALSE(tracks.moreResults);
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track2.getId());
|
EXPECT_EQ(tracks.results.front()->getId(), track2.getId());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleStarredArtist)
|
|
||||||
{
|
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
|
||||||
ScopedTrack track {session, "MyTrack"};
|
|
||||||
ScopedUser user {session, "MyUser"};
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
EXPECT_FALSE(user->isStarred(artist.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)};
|
|
||||||
user.get().modify()->star(artist.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
EXPECT_TRUE(user->isStarred(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
add_executable(test-database
|
add_executable(test-database
|
||||||
Artist.cpp
|
Artist.cpp
|
||||||
Cluster.cpp
|
Cluster.cpp
|
||||||
|
Common.cpp
|
||||||
DatabaseTest.cpp
|
DatabaseTest.cpp
|
||||||
|
Listen.cpp
|
||||||
Release.cpp
|
Release.cpp
|
||||||
|
StarredArtist.cpp
|
||||||
|
StarredRelease.cpp
|
||||||
|
StarredTrack.cpp
|
||||||
Track.cpp
|
Track.cpp
|
||||||
|
TrackBookmark.cpp
|
||||||
|
TrackFeatures.cpp
|
||||||
|
TrackList.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(test-database PRIVATE
|
target_link_libraries(test-database PRIVATE
|
||||||
|
|||||||
@@ -24,60 +24,73 @@
|
|||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleCluster)
|
TEST_F(DatabaseFixture, Cluster)
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||||
|
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
ScopedClusterType clusterType {session, "MyType"};
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
EXPECT_EQ(ClusterType::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
auto clusters {Cluster::getAll(session)};
|
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||||
ASSERT_EQ(clusters.size(), 1);
|
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||||
EXPECT_EQ(clusters.front()->getId(), cluster.getId());
|
|
||||||
EXPECT_EQ(clusters.front()->getType()->getId(), clusterType.getId());
|
|
||||||
|
|
||||||
clusters = Cluster::getAllOrphans(session);
|
auto clusters {Cluster::find(session, Range {})};
|
||||||
ASSERT_EQ(clusters.size(), 1);
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
EXPECT_EQ(clusters.front()->getId(), cluster.getId());
|
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||||
|
|
||||||
auto clusterTypes {ClusterType::getAll(session)};
|
clusters = Cluster::findOrphans(session, Range {});
|
||||||
ASSERT_EQ(clusterTypes.size(), 1);
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId());
|
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||||
|
|
||||||
clusterTypes = ClusterType::getAllUsed(session);
|
auto clusterTypes {ClusterType::find(session, Range {})};
|
||||||
ASSERT_EQ(clusterTypes.size(), 1);
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId());
|
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||||
|
|
||||||
clusterTypes = ClusterType::getAllOrphans(session);
|
clusterTypes = ClusterType::findUsed(session, Range {});
|
||||||
EXPECT_TRUE(clusterTypes.empty());
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
|
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||||
|
|
||||||
|
clusterTypes = ClusterType::findOrphans(session, Range {});
|
||||||
|
EXPECT_TRUE(clusterTypes.results.empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
auto clusterTypes {ClusterType::getAllOrphans(session)};
|
auto clusterTypes {ClusterType::findOrphans(session, Range {})};
|
||||||
ASSERT_EQ(clusterTypes.size(), 1);
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId());
|
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||||
|
|
||||||
ASSERT_TRUE(ClusterType::getAllUsed(session).empty());
|
ASSERT_TRUE(ClusterType::findUsed(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackSingleCluster)
|
TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
auto clusterTypes {ClusterType::getAllOrphans(session)};
|
auto clusterTypes {ClusterType::findOrphans(session, Range {})};
|
||||||
ASSERT_EQ(clusterTypes.size(), 1);
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId());
|
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
||||||
@@ -85,17 +98,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
auto clusters {Cluster::getAllOrphans(session)};
|
auto clusters {Cluster::findOrphans(session, Range {})};
|
||||||
EXPECT_EQ(clusters.size(), 2);
|
EXPECT_EQ(clusters.results.size(), 2);
|
||||||
EXPECT_TRUE(track->getClusters().empty());
|
EXPECT_TRUE(track->getClusters().empty());
|
||||||
EXPECT_TRUE(track->getClusterIds().empty());
|
EXPECT_TRUE(track->getClusterIds().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
EXPECT_TRUE(Track::getAllIdsWithClusters(session).empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
@@ -104,29 +112,22 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
auto tracks {Track::getAllIdsWithClusters(session)};
|
auto clusters {Cluster::findOrphans(session, Range {})};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.front(), track.getId());
|
EXPECT_EQ(clusters.results.front(), cluster2.getId());
|
||||||
}
|
|
||||||
|
|
||||||
{
|
EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty());
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
auto clusters {Cluster::getAllOrphans(session)};
|
|
||||||
ASSERT_EQ(clusters.size(), 1);
|
|
||||||
EXPECT_EQ(clusters.front()->getId(), cluster2.getId());
|
|
||||||
|
|
||||||
EXPECT_TRUE(ClusterType::getAllOrphans(session).empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto tracks {Track::getByClusters(session, {cluster1.getId()})};
|
auto tracks {Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()}))};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track.getId());
|
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||||
|
|
||||||
tracks = Track::getByClusters(session, {cluster2.getId()});
|
tracks = Track::find(session, Track::FindParameters {}.setClusters({cluster2.getId()}));
|
||||||
EXPECT_TRUE(tracks.empty());
|
EXPECT_TRUE(tracks.results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -142,7 +143,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksSingleCluster)
|
TEST_F(DatabaseFixture, Cluster_multiTracks)
|
||||||
{
|
{
|
||||||
std::list<ScopedTrack> tracks;
|
std::list<ScopedTrack> tracks;
|
||||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||||
@@ -160,19 +161,19 @@ TEST_F(DatabaseFixture, MultipleTracksSingleCluster)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
|
|
||||||
EXPECT_EQ(cluster->getTracksCount(), tracks.size());
|
EXPECT_EQ(cluster->getTracksCount(), tracks.size());
|
||||||
|
|
||||||
for (auto trackCluster : cluster->getTracks())
|
for (TrackId trackId : cluster->getTracks(Range {}).results)
|
||||||
{
|
{
|
||||||
auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackCluster->getId() == track.getId(); })};
|
auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); })};
|
||||||
EXPECT_TRUE(it != std::cend(tracks));
|
EXPECT_TRUE(it != std::cend(tracks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease)
|
TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
|
||||||
{
|
{
|
||||||
ScopedClusterType clusterType {session, "ClusterType"};
|
ScopedClusterType clusterType {session, "ClusterType"};
|
||||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"};
|
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"};
|
||||||
@@ -268,16 +269,25 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster)
|
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrackFile"};
|
ScopedTrack track {session, "MyTrackFile"};
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release {session, "MyRelease"};
|
||||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
|
||||||
ScopedCluster cluster {session, clusterType .lockAndGet(), "MyCluster"};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Release::getAllIdsWithClusters(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
ScopedCluster unusedCluster {session, clusterType.lockAndGet(), "MyClusterUnused"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
ASSERT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2);
|
||||||
|
EXPECT_TRUE(Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})).results.empty());
|
||||||
|
EXPECT_EQ(Release::find(session, Release::FindParameters {}).results.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -289,24 +299,27 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
auto releases {Release::getAllIdsWithClusters(session)};
|
|
||||||
ASSERT_EQ(releases.size(), 1);
|
{
|
||||||
EXPECT_EQ(releases.front(), release.getId());
|
auto clusters {Cluster::findOrphans(session, Range {})};
|
||||||
|
ASSERT_EQ(clusters.results.size(), 1);
|
||||||
|
EXPECT_EQ(clusters.results.front(), unusedCluster.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
auto releases {Release::find(session, Release::FindParameters {}.setClusters({cluster.getId()}))};
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results.front(), release.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto releases {Release::getByClusters(session, {cluster.getId()})};
|
auto releases {Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()}))};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -314,6 +327,8 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster)
|
|||||||
|
|
||||||
EXPECT_EQ(cluster->getReleasesCount(), 1);
|
EXPECT_EQ(cluster->getReleasesCount(), 1);
|
||||||
EXPECT_EQ(cluster->getTracksCount(), 1);
|
EXPECT_EQ(cluster->getTracksCount(), 1);
|
||||||
|
EXPECT_EQ(unusedCluster->getReleasesCount(), 0);
|
||||||
|
EXPECT_EQ(unusedCluster->getTracksCount(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,10 +349,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(ClusterType::getAllOrphans(session).empty());
|
EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_EQ(Cluster::getAllOrphans(session).size(), 2);
|
EXPECT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2);
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -349,12 +364,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
EXPECT_TRUE(Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName).empty());
|
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()})).results.empty());
|
||||||
EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty());
|
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty());
|
||||||
|
|
||||||
cluster2.get().modify()->addTrack(track.get());
|
cluster2.get().modify()->addTrack(track.get());
|
||||||
}
|
}
|
||||||
@@ -362,19 +377,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
artists = Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName);
|
artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()}));
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()}, Artist::SortMethod::ByName);
|
artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}));
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty());
|
EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,17 +410,17 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,8 +450,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -445,9 +460,9 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
|||||||
std::vector<ClusterId> clusterIds;
|
std::vector<ClusterId> clusterIds;
|
||||||
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
|
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
|
||||||
|
|
||||||
auto artists {Artist::getByClusters(session, clusterIds, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {}.setClusters(clusterIds))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,15 +486,11 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})};
|
const auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})};
|
||||||
EXPECT_EQ(similarTracks.size(), tracks.size() - 1);
|
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||||
for (auto similarTrack : similarTracks)
|
for (const TrackId similarTrackId : similarTracks.results)
|
||||||
{
|
{
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks),
|
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||||
[&](const ScopedTrack& track)
|
|
||||||
{
|
|
||||||
return similarTrack->getId() == track.getId();
|
|
||||||
}) != std::cend(tracks));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,17 +527,17 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto similarTracks {Track::getSimilarTracks(session, {tracks.back().getId()}, 0, 4)};
|
auto similarTracks {Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4})};
|
||||||
EXPECT_EQ(similarTracks.size(), 4);
|
EXPECT_EQ(similarTracks.results.size(), 4);
|
||||||
for (auto similarTrack : similarTracks)
|
for (const TrackId similarTrackId : similarTracks.results)
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks));
|
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})};
|
auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})};
|
||||||
EXPECT_EQ(similarTracks.size(), tracks.size() - 1);
|
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||||
for (auto similarTrack : similarTracks)
|
for (const TrackId similarTrackId : similarTracks.results)
|
||||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks));
|
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,11 +550,6 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
|||||||
ScopedClusterType clusterType {session, "MyType"};
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
EXPECT_TRUE(Artist::getAllIdsWithClusters(session).empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
@@ -555,25 +561,18 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
EXPECT_TRUE(Cluster::getAllOrphans(session).empty());
|
EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(ClusterType::getAllOrphans(session).empty());
|
EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
auto artists {Artist::getAllIdsWithClusters(session)};
|
|
||||||
ASSERT_EQ(artists.size(), 1);
|
|
||||||
EXPECT_EQ(artists.front(), artist.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)};
|
auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
|
|
||||||
auto releases {artist->getReleases()};
|
auto releases {artist->getReleases()};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
@@ -735,9 +734,9 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty());
|
||||||
EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
|
||||||
EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -750,15 +749,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size() , 1);
|
ASSERT_EQ(releases.size() , 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
||||||
}
|
}
|
||||||
@@ -767,15 +766,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
||||||
}
|
}
|
||||||
@@ -784,15 +783,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
||||||
}
|
}
|
||||||
@@ -801,7 +800,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
}
|
}
|
||||||
@@ -810,7 +809,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
}
|
}
|
||||||
@@ -819,11 +818,11 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
EXPECT_TRUE(trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty());
|
||||||
EXPECT_TRUE(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||||
EXPECT_TRUE(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||||
|
|
||||||
EXPECT_TRUE(trackList->getArtistsReverse({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty());
|
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -836,17 +835,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist2.getId());
|
EXPECT_EQ(artists[0]->getId(), artist2.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist1.getId());
|
EXPECT_EQ(artists[1]->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release2.getId());
|
EXPECT_EQ(releases[0]->getId(), release2.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release1.getId());
|
EXPECT_EQ(releases[1]->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track2.getId());
|
EXPECT_EQ(tracks[0]->getId(), track2.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(),track1.getId());
|
EXPECT_EQ(tracks[1]->getId(),track1.getId());
|
||||||
@@ -856,17 +855,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist2.getId());
|
EXPECT_EQ(artists[0]->getId(), artist2.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist1.getId());
|
EXPECT_EQ(artists[1]->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release2.getId());
|
EXPECT_EQ(releases[0]->getId(), release2.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release1.getId());
|
EXPECT_EQ(releases[1]->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track2.getId());
|
EXPECT_EQ(tracks[0]->getId(), track2.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(), track1.getId());
|
EXPECT_EQ(tracks[1]->getId(), track1.getId());
|
||||||
@@ -876,15 +875,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
EXPECT_EQ(tracks.front()->getId(), track1.getId());
|
||||||
}
|
}
|
||||||
@@ -893,15 +892,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track2.getId());
|
EXPECT_EQ(tracks.front()->getId(), track2.getId());
|
||||||
}
|
}
|
||||||
@@ -916,17 +915,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist2.getId());
|
EXPECT_EQ(artists[1]->getId(), artist2.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release2.getId());
|
EXPECT_EQ(releases[1]->getId(), release2.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track1.getId());
|
EXPECT_EQ(tracks[0]->getId(), track1.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(), track2.getId());
|
EXPECT_EQ(tracks[1]->getId(), track2.getId());
|
||||||
@@ -936,17 +935,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool moreResults {};
|
bool moreResults {};
|
||||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist2.getId());
|
EXPECT_EQ(artists[1]->getId(), artist2.getId());
|
||||||
|
|
||||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release2.getId());
|
EXPECT_EQ(releases[1]->getId(), release2.getId());
|
||||||
|
|
||||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track1.getId());
|
EXPECT_EQ(tracks[0]->getId(), track1.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(), track2.getId());
|
EXPECT_EQ(tracks[1]->getId(), track2.getId());
|
||||||
@@ -964,9 +963,9 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(artist1->getSimilarArtists().empty());
|
EXPECT_TRUE(artist1->findSimilarArtists().results.empty());
|
||||||
EXPECT_TRUE(artist2->getSimilarArtists().empty());
|
EXPECT_TRUE(artist2->findSimilarArtists().results.empty());
|
||||||
EXPECT_TRUE(artist3->getSimilarArtists().empty());
|
EXPECT_TRUE(artist3->findSimilarArtists().results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::list<ScopedTrack> tracks;
|
std::list<ScopedTrack> tracks;
|
||||||
@@ -998,38 +997,38 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist1->getSimilarArtists()};
|
auto artists {artist1->findSimilarArtists()};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})};
|
auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist})};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::ReleaseArtist})};
|
auto artists {artist1->findSimilarArtists({TrackArtistLinkType::ReleaseArtist})};
|
||||||
EXPECT_EQ(artists.empty(), 1);
|
EXPECT_EQ(artists.results.empty(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})};
|
auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})};
|
||||||
ASSERT_EQ(artists.size(), 1);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Composer})};
|
auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Composer})};
|
||||||
EXPECT_TRUE(artists.empty());
|
EXPECT_TRUE(artists.results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto artists {artist2->getSimilarArtists()};
|
auto artists {artist2->findSimilarArtists()};
|
||||||
ASSERT_EQ(artists.size(), 2);
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
EXPECT_EQ(artists[0]->getId(), artist1.getId());
|
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||||
EXPECT_EQ(artists[1]->getId(), artist3.getId());
|
EXPECT_EQ(artists.results[1], artist3.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/StarredArtist.hpp"
|
||||||
|
#include "services/database/StarredRelease.hpp"
|
||||||
|
#include "services/database/StarredTrack.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "services/database/TrackBookmark.hpp"
|
||||||
|
#include "services/database/TrackList.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
TmpDatabase::TmpDatabase()
|
||||||
|
: _tmpFile {std::tmpnam(nullptr)}
|
||||||
|
, _fileDeleter {_tmpFile}
|
||||||
|
, _db {_tmpFile}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::Db&
|
||||||
|
TmpDatabase::getDb()
|
||||||
|
{
|
||||||
|
return _db;
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseFixture::~DatabaseFixture()
|
||||||
|
{
|
||||||
|
testDatabaseEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DatabaseFixture::SetUpTestCase()
|
||||||
|
{
|
||||||
|
_tmpDb = std::make_unique<TmpDatabase>();
|
||||||
|
{
|
||||||
|
Database::Session s {_tmpDb->getDb()};
|
||||||
|
s.prepareTables();
|
||||||
|
s.optimize();
|
||||||
|
|
||||||
|
// remove default created entries
|
||||||
|
{
|
||||||
|
auto transaction {s.createUniqueTransaction()};
|
||||||
|
auto clusterTypes {Database::ClusterType::find(s, Database::Range {})};
|
||||||
|
for (auto clusterTypeId : clusterTypes.results)
|
||||||
|
{
|
||||||
|
auto clusterType {Database::ClusterType::find(s, clusterTypeId)};
|
||||||
|
clusterType.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DatabaseFixture::TearDownTestCase()
|
||||||
|
{
|
||||||
|
_tmpDb.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
DatabaseFixture::testDatabaseEmpty()
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
auto uniqueTransaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Artist::getCount(session), 0);
|
||||||
|
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||||
|
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||||
|
EXPECT_EQ(Listen::getCount(session), 0);
|
||||||
|
EXPECT_EQ(Release::getCount(session), 0);
|
||||||
|
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||||
|
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||||
|
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||||
|
EXPECT_EQ(Track::getCount(session), 0);
|
||||||
|
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||||
|
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||||
|
EXPECT_EQ(User::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Common_subRangeEmpty)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
RangeResults<int> results;
|
||||||
|
results.range = Range {0, 0};
|
||||||
|
results.results = {};
|
||||||
|
results.moreResults = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {0, 0})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 0);
|
||||||
|
EXPECT_EQ(subRange.range, Range {});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {0, 1})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Common_IdType)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
{
|
||||||
|
const IdType id{};
|
||||||
|
EXPECT_FALSE(id.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const IdType id{0};
|
||||||
|
EXPECT_TRUE(id.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const IdType id1{0};
|
||||||
|
const IdType id2{0};
|
||||||
|
EXPECT_EQ(id1, id2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const IdType id1{0};
|
||||||
|
const IdType id2{1};
|
||||||
|
EXPECT_NE(id1, id2);
|
||||||
|
EXPECT_LT(id1, id2);
|
||||||
|
EXPECT_GT(id2, id1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Common_subRange)
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
RangeResults<int> results;
|
||||||
|
results.range = Range {0, 2};
|
||||||
|
results.results = {5, 6};
|
||||||
|
results.moreResults = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {0, 1})};
|
||||||
|
EXPECT_TRUE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 1);
|
||||||
|
EXPECT_EQ(subRange.results.front(), 5);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {1, 1})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 1);
|
||||||
|
EXPECT_EQ(subRange.results.front(), 6);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {0, 2})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 2);
|
||||||
|
EXPECT_EQ(subRange.results.front(), 5);
|
||||||
|
EXPECT_EQ(subRange.results.back(), 6);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 2);
|
||||||
|
EXPECT_EQ(subRange.results.front(), 5);
|
||||||
|
EXPECT_EQ(subRange.results.back(), 6);
|
||||||
|
EXPECT_EQ(subRange.range, results.range);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {1, 0})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 1);
|
||||||
|
EXPECT_EQ(subRange.results.front(), 6);
|
||||||
|
const Range expectedRange {1, 1};
|
||||||
|
EXPECT_EQ(subRange.range, expectedRange);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto subRange {results.getSubRange(Range {3, 2})};
|
||||||
|
EXPECT_FALSE(subRange.moreResults);
|
||||||
|
ASSERT_EQ(subRange.results.size(), 0);
|
||||||
|
const Range expectedRange {2, 0};
|
||||||
|
EXPECT_EQ(subRange.range, expectedRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -27,11 +27,13 @@
|
|||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Cluster.hpp"
|
#include "services/database/Cluster.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/database/TrackBookmark.hpp"
|
#include "services/database/TrackBookmark.hpp"
|
||||||
|
#include "services/database/TrackFeatures.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
@@ -57,7 +59,7 @@ class ScopedEntity
|
|||||||
{
|
{
|
||||||
auto transaction {_session.createUniqueTransaction()};
|
auto transaction {_session.createUniqueTransaction()};
|
||||||
|
|
||||||
auto entity {T::getById(_session, _id)};
|
auto entity {T::find(_session, _id)};
|
||||||
entity.remove();
|
entity.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ class ScopedEntity
|
|||||||
{
|
{
|
||||||
_session.checkSharedLocked();
|
_session.checkSharedLocked();
|
||||||
|
|
||||||
auto entity {T::getById(_session, _id)};
|
auto entity {T::find(_session, _id)};
|
||||||
EXPECT_TRUE(entity);
|
EXPECT_TRUE(entity);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,6 @@ using ScopedCluster = ScopedEntity<Database::Cluster>;
|
|||||||
using ScopedClusterType = ScopedEntity<Database::ClusterType>;
|
using ScopedClusterType = ScopedEntity<Database::ClusterType>;
|
||||||
using ScopedRelease = ScopedEntity<Database::Release>;
|
using ScopedRelease = ScopedEntity<Database::Release>;
|
||||||
using ScopedTrack = ScopedEntity<Database::Track>;
|
using ScopedTrack = ScopedEntity<Database::Track>;
|
||||||
using ScopedTrackBookmark = ScopedEntity<Database::TrackBookmark>;
|
|
||||||
using ScopedTrackList = ScopedEntity<Database::TrackList>;
|
using ScopedTrackList = ScopedEntity<Database::TrackList>;
|
||||||
using ScopedUser = ScopedEntity<Database::User>;
|
using ScopedUser = ScopedEntity<Database::User>;
|
||||||
|
|
||||||
@@ -120,60 +121,27 @@ class ScopedFileDeleter final
|
|||||||
class TmpDatabase final
|
class TmpDatabase final
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Database::Db& getDb() { return _db; }
|
TmpDatabase ();
|
||||||
|
|
||||||
|
Database::Db& getDb();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::filesystem::path _tmpFile {std::tmpnam(nullptr)};
|
const std::filesystem::path _tmpFile;
|
||||||
ScopedFileDeleter fileDeleter {_tmpFile};
|
ScopedFileDeleter _fileDeleter;
|
||||||
Database::Db _db {_tmpFile};
|
Database::Db _db;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DatabaseFixture : public ::testing::Test
|
class DatabaseFixture : public ::testing::Test
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
~DatabaseFixture()
|
~DatabaseFixture();
|
||||||
{
|
|
||||||
testDatabaseEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static void SetUpTestCase()
|
static void SetUpTestCase();
|
||||||
{
|
static void TearDownTestCase();
|
||||||
_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:
|
private:
|
||||||
void testDatabaseEmpty()
|
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 {};
|
static inline std::unique_ptr<TmpDatabase> _tmpDb {};
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(Artist::getAllOrphans(session).empty());
|
EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -89,8 +89,8 @@ TEST_F(DatabaseFixture, SingleUser)
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(User::getAll(session).empty());
|
EXPECT_TRUE(User::find(session, Range {}).results.empty());
|
||||||
EXPECT_TRUE(User::getAllIds(session).empty());
|
EXPECT_EQ(User::getCount(session), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user {session, "MyUser"};
|
||||||
@@ -98,214 +98,8 @@ TEST_F(DatabaseFixture, SingleUser)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
EXPECT_EQ(user->getQueuedTrackList(session)->getCount(), 0);
|
EXPECT_EQ(User::find(session, Range {}).results.size(), 1);
|
||||||
EXPECT_EQ(User::getAll(session).size(), 1);
|
EXPECT_EQ(User::getCount(session), 1);
|
||||||
EXPECT_EQ(User::getAllIds(session).size(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,892 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/Listen.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
using ScopedListen = ScopedEntity<Database::Listen>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getAll)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Listen::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Listen::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
ScopedListen listen2 {session, user.get(), track.get(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}}};
|
||||||
|
|
||||||
|
EXPECT_EQ(Listen::getCount(session), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Listen::getCount(session), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_get)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto listens {Listen::find(session, user->getId(), Scrobbler::ListenBrainz)};
|
||||||
|
EXPECT_EQ(listens.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto listens {Listen::find(session, user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_EQ(listens.moreResults, false);
|
||||||
|
ASSERT_EQ(listens.results.size(), 1);
|
||||||
|
EXPECT_EQ(listens.results.front()->getId(), listen->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_get_multi)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}};
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto listens {Listen::find(session, user->getId(), Scrobbler::Internal)};
|
||||||
|
ASSERT_EQ(listens.results.size(), 3);
|
||||||
|
EXPECT_EQ(listens.results[0]->getId(), listen1.getId());
|
||||||
|
EXPECT_EQ(listens.results[1]->getId(), listen2.getId());
|
||||||
|
EXPECT_EQ(listens.results[2]->getId(), listen3.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_get_byDateTime)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
const Wt::WDateTime dateTime2 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2}};
|
||||||
|
ASSERT_GT(dateTime2, dateTime1);
|
||||||
|
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1};
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
{
|
||||||
|
Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime1)};
|
||||||
|
ASSERT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getId(), listen1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2)};
|
||||||
|
ASSERT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getId(), listen2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2.addSecs(56))};
|
||||||
|
EXPECT_FALSE(listen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime1};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
EXPECT_EQ(artists.moreResults, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime1.addSecs(1)};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedArtist artist2 {session, "MyArtist2"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||||
|
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)};
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
|
EXPECT_EQ(artists.results[1], artist1->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.moreResults, true);
|
||||||
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopReleases)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::ListenBrainz, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedRelease release1 {session, "MyRelease1"};
|
||||||
|
ScopedRelease release2 {session, "MyRelease2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track1.get().modify()->setRelease(release1.get());
|
||||||
|
track2.get().modify()->setRelease(release2.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
|
EXPECT_EQ(releases.results[1], release1.getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
|
EXPECT_EQ(releases.results[1], release2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::ListenBrainz, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], track1.getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], track2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
EXPECT_EQ(artists.moreResults, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedArtist artist2 {session, "MyArtist2"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||||
|
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
|
EXPECT_EQ(artists.results[1], artist1->getId());
|
||||||
|
}
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.moreResults, true);
|
||||||
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)};
|
||||||
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
|
EXPECT_EQ(artists.results[0], artist.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::ListenBrainz, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedRelease release1 {session, "MyRelease1"};
|
||||||
|
ScopedRelease release2 {session, "MyRelease2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track1.get().modify()->setRelease(release1.get());
|
||||||
|
track2.get().modify()->setRelease(release2.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
|
EXPECT_EQ(releases.results[1], release2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
|
EXPECT_EQ(releases.results[1], release1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
|
EXPECT_EQ(releases.results[1], release1.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::ListenBrainz, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], track2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], track1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})};
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], track1.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}};
|
||||||
|
ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime};
|
||||||
|
ScopedClusterType clusterType {session, "MyType"};
|
||||||
|
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
cluster.get().modify()->addTrack(track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,11 +21,12 @@
|
|||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleRelease)
|
TEST_F(DatabaseFixture, Release)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Release::getCount(session), 0);
|
||||||
EXPECT_FALSE(Release::exists(session, 0));
|
EXPECT_FALSE(Release::exists(session, 0));
|
||||||
EXPECT_FALSE(Release::exists(session, 1));
|
EXPECT_FALSE(Release::exists(session, 1));
|
||||||
}
|
}
|
||||||
@@ -35,20 +36,23 @@ TEST_F(DatabaseFixture, SingleRelease)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(Release::getCount(session), 1);
|
||||||
EXPECT_TRUE(Release::exists(session, release.getId()));
|
EXPECT_TRUE(Release::exists(session, release.getId()));
|
||||||
|
|
||||||
auto releases {Release::getAllOrphans(session)};
|
auto releases {Release::findOrphans(session, Range {})};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
EXPECT_EQ(releases.results.front(), release.getId());
|
||||||
|
|
||||||
releases = Release::getAll(session);
|
{
|
||||||
ASSERT_EQ(releases.size(), 1);
|
auto releases {Release::find(session, Release::FindParameters {})};
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results.front(), release.getId());
|
||||||
EXPECT_EQ(release->getDuration(), std::chrono::seconds {0});
|
EXPECT_EQ(release->getDuration(), std::chrono::seconds {0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackSingleRelease)
|
TEST_F(DatabaseFixture, Release_singleTrack)
|
||||||
{
|
{
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
EXPECT_TRUE(Release::getAllOrphans(session).empty());
|
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||||
|
|
||||||
EXPECT_EQ(release->getTracksCount(), 1);
|
EXPECT_EQ(release->getTracksCount(), 1);
|
||||||
ASSERT_EQ(release->getTracks().size(), 1);
|
ASSERT_EQ(release->getTracks().size(), 1);
|
||||||
@@ -80,19 +84,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")};
|
auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")};
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.front()->getId(), track.getId());
|
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")};
|
auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")};
|
||||||
EXPECT_EQ(tracks.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")};
|
auto tracks {Track::findByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")};
|
||||||
EXPECT_EQ(tracks.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +105,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease)
|
|||||||
|
|
||||||
EXPECT_TRUE(release->getTracks().empty());
|
EXPECT_TRUE(release->getTracks().empty());
|
||||||
|
|
||||||
auto releases {Release::getAllOrphans(session)};
|
auto releases {Release::findOrphans(session, Range {})};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
EXPECT_EQ(releases.results.front(), release.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,38 +142,37 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool more;
|
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Release"}))};
|
||||||
EXPECT_EQ(releases.size(), 6);
|
EXPECT_EQ(releases.results.size(), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease"}))};
|
||||||
EXPECT_EQ(releases.size(), 5);
|
ASSERT_EQ(releases.results.size(), 5);
|
||||||
EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); }));
|
EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); }));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%"}))};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release2.getId());
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release4.getId());
|
EXPECT_EQ(releases.results[1], release4.getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"%MyRelease"}))};
|
||||||
ASSERT_EQ(releases.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases[0]->getId(), release3.getId());
|
EXPECT_EQ(releases.results[0], release3.getId());
|
||||||
EXPECT_EQ(releases[1]->getId(), release5.getId());
|
EXPECT_EQ(releases.results[1], release5.getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"}))};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases[0]->getId(), release5.getId());
|
EXPECT_EQ(releases.results[0], release5.getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"}))};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases[0]->getId(), release4.getId());
|
EXPECT_EQ(releases.results[0], release4.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,8 +326,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const auto releases {Release::getByYear(session, 0, 3000)};
|
const auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000)))};
|
||||||
EXPECT_EQ(releases.size(), 0);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -348,48 +351,48 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto releases {Release::getByYear(session, 1950, 2000)};
|
auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000)))};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||||
|
|
||||||
releases = Release::getByYear(session, 1994, 1994);
|
releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1994, 1994)));
|
||||||
ASSERT_EQ(releases.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||||
|
|
||||||
releases = Release::getByYear(session, 1993, 1993);
|
releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1993, 1993)));
|
||||||
ASSERT_EQ(releases.size(), 0);
|
ASSERT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleStarredRelease)
|
TEST_F(DatabaseFixture, Release_writtenAfter)
|
||||||
{
|
{
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release {session, "MyRelease"};
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedUser user {session, "MyUser"};
|
|
||||||
|
|
||||||
{
|
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}};
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
EXPECT_FALSE(user->isStarred(release.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track.get().modify()->setLastWriteTime(dateTime);
|
||||||
track.get().modify()->setRelease(release.get());
|
track.get().modify()->setRelease(release.get());
|
||||||
user.get().modify()->star(release.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
const auto releases {Release::find(session, Release::FindParameters {})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
EXPECT_TRUE(user->isStarred(release.get()));
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
bool hasMore {};
|
{
|
||||||
auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
|
auto transaction {session.createSharedTransaction()};
|
||||||
ASSERT_EQ(releases.size(), 1);
|
const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
|
||||||
EXPECT_EQ(releases.front()->getId(), release.getId());
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
EXPECT_FALSE(hasMore);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredArtist.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
using ScopedStarredArtist = ScopedEntity<Database::StarredArtist>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredArtist)
|
||||||
|
{
|
||||||
|
ScopedArtist artist {session, "MyArtist"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedUser user2 {session, "MyUser2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_FALSE(starredArtist);
|
||||||
|
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||||
|
|
||||||
|
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||||
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||||
|
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Artist::find(session, Artist::FindParameters {})};
|
||||||
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
|
||||||
|
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
|
||||||
|
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||||
|
{
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedArtist artist2 {session, "MyArtist2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||||
|
EXPECT_EQ(artists.results.size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
|
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
|
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
||||||
|
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
|
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
|
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
||||||
|
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredRelease.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredRelease)
|
||||||
|
{
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedUser user2 {session, "MyUser2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_FALSE(starredRelease);
|
||||||
|
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||||
|
|
||||||
|
auto releases {Release::find(session, Release::FindParameters {})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||||
|
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Release::find(session, Release::FindParameters {})};
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
|
||||||
|
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
|
||||||
|
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||||
|
{
|
||||||
|
ScopedRelease release1 {session, "MyRelease1"};
|
||||||
|
ScopedRelease release2 {session, "MyRelease2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||||
|
EXPECT_EQ(releases.results.size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
|
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
||||||
|
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
|
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
|
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
||||||
|
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/StarredTrack.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedUser user2 {session, "MyUser2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_FALSE(starredTrack);
|
||||||
|
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||||
|
|
||||||
|
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
||||||
|
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||||
|
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
|
||||||
|
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
|
||||||
|
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
||||||
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||||
|
{
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||||
|
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
||||||
|
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||||
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
|
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||||
|
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrack)
|
TEST_F(DatabaseFixture, Track)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0);
|
||||||
EXPECT_EQ(Track::getCount(session), 0);
|
EXPECT_EQ(Track::getCount(session), 0);
|
||||||
EXPECT_FALSE(Track::exists(session, 0));
|
EXPECT_FALSE(Track::exists(session, 0));
|
||||||
}
|
}
|
||||||
@@ -36,16 +37,16 @@ TEST_F(DatabaseFixture, SingleTrack)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
EXPECT_EQ(Track::getAll(session).size(), 1);
|
EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1);
|
||||||
EXPECT_EQ(Track::getCount(session), 1);
|
EXPECT_EQ(Track::getCount(session), 1);
|
||||||
EXPECT_TRUE(Track::exists(session, track.getId()));
|
EXPECT_TRUE(Track::exists(session, track.getId()));
|
||||||
auto myTrack {Track::getById(session, track.getId())};
|
auto myTrack {Track::find(session, track.getId())};
|
||||||
ASSERT_TRUE(myTrack);
|
ASSERT_TRUE(myTrack);
|
||||||
EXPECT_EQ(myTrack->getId(), track.getId());
|
EXPECT_EQ(myTrack->getId(), track.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
TEST_F(DatabaseFixture, Track_findByKeywords)
|
||||||
{
|
{
|
||||||
ScopedTrack track1 {session, ""};
|
ScopedTrack track1 {session, ""};
|
||||||
ScopedTrack track2 {session, ""};
|
ScopedTrack track2 {session, ""};
|
||||||
@@ -67,32 +68,31 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
bool more;
|
|
||||||
{
|
{
|
||||||
const auto tracks {Track::getByFilter(session, {}, {"Track"}, std::nullopt, more)};
|
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))};
|
||||||
EXPECT_EQ(tracks.size(), 6);
|
EXPECT_EQ(tracks.results.size(), 6);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto tracks {Track::getByFilter(session, {}, {"MyTrack"}, std::nullopt, more)};
|
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))};
|
||||||
EXPECT_EQ(tracks.size(), 5);
|
EXPECT_EQ(tracks.results.size(), 5);
|
||||||
EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track->getId() == track6.getId(); }));
|
EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); }));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto tracks {Track::getByFilter(session, {}, {"MyTrack%"}, std::nullopt, more)};
|
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track2.getId());
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(), track3.getId());
|
EXPECT_EQ(tracks.results[1], track3.getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto tracks {Track::getByFilter(session, {}, {"%MyTrack"}, std::nullopt, more)};
|
const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))};
|
||||||
ASSERT_EQ(tracks.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks[0]->getId(), track4.getId());
|
EXPECT_EQ(tracks.results[0], track4.getId());
|
||||||
EXPECT_EQ(tracks[1]->getId(), track5.getId());
|
EXPECT_EQ(tracks.results[1], track5.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleTrackDate)
|
TEST_F(DatabaseFixture, Track_date)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
|
||||||
@@ -115,36 +115,33 @@ TEST_F(DatabaseFixture, SingleTrackDate)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, SingleStarredTrack)
|
TEST_F(DatabaseFixture, Track_writtenAfter)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedUser user {session, "MyUser"};
|
|
||||||
|
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
track.get().modify()->setLastWriteTime(dateTime);
|
||||||
EXPECT_FALSE(user->isStarred(track.get()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
const auto tracks {Track::find(session, Track::FindParameters {})};
|
||||||
user.get().modify()->star(track.get());
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))};
|
||||||
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
EXPECT_TRUE(user->isStarred(track.get()));
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
bool hasMore {};
|
const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))};
|
||||||
auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
ASSERT_EQ(tracks.size(), 1);
|
|
||||||
EXPECT_EQ(tracks.front()->getId(), track.getId());
|
|
||||||
EXPECT_FALSE(hasMore);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "services/database/TrackBookmark.hpp"
|
||||||
|
|
||||||
|
using ScopedTrackBookmark = ScopedEntity<Database::TrackBookmark>;
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackBookmark)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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::getCount(session), 1);
|
||||||
|
|
||||||
|
const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})};
|
||||||
|
ASSERT_EQ(bookmarks.results.size(), 1);
|
||||||
|
EXPECT_EQ(bookmarks.results.front(), bookmark.getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto userBookmark {TrackBookmark::find(session, user.getId(), track.getId())};
|
||||||
|
ASSERT_TRUE(userBookmark);
|
||||||
|
EXPECT_EQ(userBookmark, bookmark.get());
|
||||||
|
|
||||||
|
EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5});
|
||||||
|
EXPECT_EQ(userBookmark->getComment(), "MyComment");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "services/database/TrackFeatures.hpp"
|
||||||
|
|
||||||
|
using ScopedTrackFeatures = ScopedEntity<Database::TrackFeatures>;
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackFeatures)
|
||||||
|
{
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(TrackFeatures::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedTrackFeatures trackFeatures {session, track.lockAndGet(), ""};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
EXPECT_EQ(TrackFeatures::getCount(session), 1);
|
||||||
|
|
||||||
|
auto allTrackFeatures {TrackFeatures::find(session, Range {})};
|
||||||
|
ASSERT_EQ(allTrackFeatures.results.size(), 1);
|
||||||
|
EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, SingleTrackList)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EXPECT_EQ(TrackList::getCount(session), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()};
|
||||||
|
ASSERT_EQ(trackIds.size(), tracks.size());
|
||||||
|
|
||||||
|
// Same order
|
||||||
|
std::size_t i {};
|
||||||
|
for (const ScopedTrack& track : tracks)
|
||||||
|
EXPECT_EQ(track.getId(), trackIds[i++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
|
ASSERT_EQ(reverseTracks.size(), tracks.size());
|
||||||
|
|
||||||
|
// Reverse order
|
||||||
|
ASSERT_TRUE(tracks.size() > 0);
|
||||||
|
std::size_t i {tracks.size() - 1};
|
||||||
|
for (const ScopedTrack& track : tracks)
|
||||||
|
EXPECT_EQ(track.getId(), reverseTracks[i--]->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()->getTracksOrderedByRecentFirst({}, 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->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty());
|
||||||
|
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
|
||||||
|
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, 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->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
|
||||||
|
ASSERT_EQ(artists.size(), 1);
|
||||||
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
|
||||||
|
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
|
||||||
|
ASSERT_EQ(releases.size(), 1);
|
||||||
|
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||||
|
|
||||||
|
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, 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->getArtistsOrderedByRecentFirst({}, 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->getReleasesOrderedByRecentFirst({}, 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->getTracksOrderedByRecentFirst({}, 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->getArtistsOrderedByRecentFirst({}, 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->getReleasesOrderedByRecentFirst({}, 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->getTracksOrderedByRecentFirst({}, 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, SingleTrackList_getArtists)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
ScopedArtist artist1 {session, "MyArtist1"};
|
||||||
|
ScopedArtist artist2 {session, "MyArtist2"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(trackList->getCount(), 0);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_TRUE(artists.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||||
|
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(trackList->getCount(), 1);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(artists.size(), 1);
|
||||||
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
|
||||||
|
EXPECT_TRUE(artists.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(artists.size(), 1);
|
||||||
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||||
|
TrackListEntry::create(session, track2.get(), trackList.get());
|
||||||
|
artist1.get().modify()->setSortName("ZZZ");
|
||||||
|
artist2.get().modify()->setSortName("AAA");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(artists.size(), 2);
|
||||||
|
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||||
|
EXPECT_EQ(artists.back()->getId(), artist2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
bool moreResults {};
|
||||||
|
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(artists.size(), 2);
|
||||||
|
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||||
|
EXPECT_EQ(artists.back()->getId(), artist1.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, SingleTrackList_getReleases)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
ScopedTrack track {session, "MyTrack"};
|
||||||
|
ScopedRelease release {session, "MyRelease"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(trackList->getCount(), 0);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_TRUE(releases.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
|
||||||
|
TrackListEntry::create(session, track.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
ASSERT_EQ(trackList->getCount(), 1);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(releases.size(), 1);
|
||||||
|
ASSERT_EQ(releases.front()->getId(), release->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, SingleTrackList_getTracks)
|
||||||
|
{
|
||||||
|
ScopedUser user {session, "MyUser"};
|
||||||
|
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
|
ScopedTrack track2 {session, "MyTrack1"};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
EXPECT_EQ(trackList->getCount(), 0);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_TRUE(tracks.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, track2.get(), trackList.get());
|
||||||
|
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
ASSERT_EQ(trackList->getCount(), 3);
|
||||||
|
bool moreResults {};
|
||||||
|
const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)};
|
||||||
|
ASSERT_EQ(tracks.size(), 2);
|
||||||
|
ASSERT_EQ(tracks[0]->getId(), track1->getId());
|
||||||
|
ASSERT_EQ(tracks[1]->getId(), track2->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
add_library(lmsfeedback SHARED
|
|
||||||
impl/FeedbackService.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(lmsfeedback INTERFACE
|
|
||||||
include
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(lmsfeedback PRIVATE
|
|
||||||
include
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(lmsfeedback PRIVATE
|
|
||||||
lmsdatabase
|
|
||||||
)
|
|
||||||
|
|
||||||
install(TARGETS lmsfeedback DESTINATION lib)
|
|
||||||
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "FeedbackService.hpp"
|
|
||||||
#include "services/database/Db.hpp"
|
|
||||||
#include "services/database/Artist.hpp"
|
|
||||||
#include "services/database/Release.hpp"
|
|
||||||
#include "services/database/Session.hpp"
|
|
||||||
#include "services/database/Track.hpp"
|
|
||||||
#include "services/database/User.hpp"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
void
|
|
||||||
exec(Db& db, UserId userId, ArtistId artistId, std::function<void(User::pointer, Artist::pointer)> func)
|
|
||||||
{
|
|
||||||
Session& dbSession {db.getTLSSession()};
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(dbSession, userId)};
|
|
||||||
if (!user)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Database::Artist::pointer artist {Database::Artist::getById(dbSession, artistId)};
|
|
||||||
if (!artist)
|
|
||||||
return;
|
|
||||||
|
|
||||||
func(user, artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
exec(Db& db, UserId userId, ReleaseId releaseId, std::function<void(User::pointer, Release::pointer)> func)
|
|
||||||
{
|
|
||||||
Session& dbSession {db.getTLSSession()};
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(dbSession, userId)};
|
|
||||||
if (!user)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Database::Release::pointer release {Database::Release::getById(dbSession, releaseId)};
|
|
||||||
if (!release)
|
|
||||||
return;
|
|
||||||
|
|
||||||
func(user, release);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
exec(Db& db, UserId userId, TrackId trackId, std::function<void(User::pointer, Track::pointer)> func)
|
|
||||||
{
|
|
||||||
Session& dbSession {db.getTLSSession()};
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(dbSession, userId)};
|
|
||||||
if (!user)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Database::Track::pointer track {Database::Track::getById(dbSession, trackId)};
|
|
||||||
if (!track)
|
|
||||||
return;
|
|
||||||
|
|
||||||
func(user, track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Feedback
|
|
||||||
{
|
|
||||||
std::unique_ptr<IFeedbackService>
|
|
||||||
createFeedbackService(Database::Db& db)
|
|
||||||
{
|
|
||||||
return std::make_unique<FeedbackService>(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedbackService::FeedbackService(Database::Db& db)
|
|
||||||
: _db {db}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::star(Database::UserId userId, Database::ArtistId artistId)
|
|
||||||
{
|
|
||||||
starObject(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::unstar(Database::UserId userId, Database::ArtistId artistId)
|
|
||||||
{
|
|
||||||
unstarObject(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
FeedbackService::isStarred(Database::UserId userId, Database::ArtistId artistId)
|
|
||||||
{
|
|
||||||
return isObjectStarred(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::star(Database::UserId userId, Database::ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
starObject(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::unstar(Database::UserId userId, Database::ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
unstarObject(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
FeedbackService::isStarred(Database::UserId userId, Database::ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
return isObjectStarred(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::star(Database::UserId userId, Database::TrackId trackId)
|
|
||||||
{
|
|
||||||
starObject(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
FeedbackService::unstar(Database::UserId userId, Database::TrackId trackId)
|
|
||||||
{
|
|
||||||
unstarObject(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
FeedbackService::isStarred(Database::UserId userId, Database::TrackId trackId)
|
|
||||||
{
|
|
||||||
return isObjectStarred(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename DatabaseId>
|
|
||||||
void
|
|
||||||
FeedbackService::starObject(Database::UserId userId, DatabaseId objectId)
|
|
||||||
{
|
|
||||||
exec(_db, userId, objectId, [](Database::User::pointer user, auto object)
|
|
||||||
{
|
|
||||||
user.modify()->star(object);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename DatabaseId>
|
|
||||||
void
|
|
||||||
FeedbackService::unstarObject(Database::UserId userId, DatabaseId objectId)
|
|
||||||
{
|
|
||||||
exec(_db, userId, objectId, [](Database::User::pointer user, auto object)
|
|
||||||
{
|
|
||||||
user.modify()->unstar(object);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename DatabaseId>
|
|
||||||
bool
|
|
||||||
FeedbackService::isObjectStarred(Database::UserId userId, DatabaseId objectId)
|
|
||||||
{
|
|
||||||
bool res {};
|
|
||||||
exec(_db, userId, objectId, [&res](Database::User::pointer user, auto object)
|
|
||||||
{
|
|
||||||
res = user->isStarred(object);
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
} // ns Feedback
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "services/feedback/IFeedbackService.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
|
||||||
{
|
|
||||||
class Db;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Feedback
|
|
||||||
{
|
|
||||||
class FeedbackService : public IFeedbackService
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
FeedbackService(Database::Db& db);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void star(Database::UserId userId, Database::ArtistId artistId) override;
|
|
||||||
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
|
|
||||||
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
|
|
||||||
|
|
||||||
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
|
|
||||||
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
|
|
||||||
bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override;
|
|
||||||
|
|
||||||
void star(Database::UserId userId, Database::TrackId trackId) override;
|
|
||||||
void unstar(Database::UserId userId, Database::TrackId trackId) override;
|
|
||||||
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
|
|
||||||
|
|
||||||
template <typename DatabaseId>
|
|
||||||
void starObject(Database::UserId userId, DatabaseId objectId);
|
|
||||||
template <typename DatabaseId>
|
|
||||||
void unstarObject(Database::UserId userId, DatabaseId objectId);
|
|
||||||
template <typename DatabaseId>
|
|
||||||
bool isObjectStarred(Database::UserId userId, DatabaseId objectId);
|
|
||||||
|
|
||||||
Database::Db& _db;
|
|
||||||
};
|
|
||||||
} // ns Feedback
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "services/database/Types.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
|
||||||
{
|
|
||||||
class Db;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Feedback
|
|
||||||
{
|
|
||||||
class IFeedbackService
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
virtual ~IFeedbackService() = default;
|
|
||||||
|
|
||||||
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
|
|
||||||
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
|
|
||||||
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
|
|
||||||
|
|
||||||
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
|
||||||
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
|
||||||
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
|
|
||||||
|
|
||||||
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
|
|
||||||
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
|
|
||||||
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unique_ptr<IFeedbackService> createFeedbackService(Database::Db& db);
|
|
||||||
} // ns Feedback
|
|
||||||
|
|
||||||
@@ -19,12 +19,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string_view>
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "services/database/TrackListId.hpp"
|
||||||
#include "services/recommendation/Types.hpp"
|
#include "services/recommendation/Types.hpp"
|
||||||
|
#include "utils/EnumSet.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -41,8 +40,8 @@ namespace Recommendation
|
|||||||
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
||||||
virtual void requestCancelLoad() = 0;
|
virtual void requestCancelLoad() = 0;
|
||||||
|
|
||||||
virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
||||||
virtual TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
virtual TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
||||||
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
||||||
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace Recommendation
|
|||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
RecommendationService::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
|
RecommendationService::findSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ namespace Recommendation
|
|||||||
if (itEngine == std::cend(_engines))
|
if (itEngine == std::cend(_engines))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
res = itEngine->second->getSimilarTracksFromTrackList(trackListId, maxCount);
|
res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount);
|
||||||
if (!res.empty())
|
if (!res.empty())
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ namespace Recommendation
|
|||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
RecommendationService::getSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
RecommendationService::findSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ namespace Recommendation
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
const IEngine& engine {*itEngine->second};
|
const IEngine& engine {*itEngine->second};
|
||||||
res = engine.getSimilarTracks(trackIds, maxCount);
|
res = engine.findSimilarTracks(trackIds, maxCount);
|
||||||
if (!res.empty())
|
if (!res.empty())
|
||||||
{
|
{
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'";
|
||||||
@@ -140,6 +140,8 @@ namespace Recommendation
|
|||||||
if (itEngine == std::cend(_engines))
|
if (itEngine == std::cend(_engines))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "'";
|
||||||
|
|
||||||
const IEngine& engine {*itEngine->second};
|
const IEngine& engine {*itEngine->second};
|
||||||
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
|
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
|
||||||
if (!res.empty())
|
if (!res.empty())
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ namespace Recommendation
|
|||||||
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||||
void cancelLoad() override;
|
void cancelLoad() override;
|
||||||
|
|
||||||
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||||
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||||
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||||
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||||
|
|
||||||
|
|||||||
@@ -29,40 +29,35 @@
|
|||||||
|
|
||||||
namespace Recommendation {
|
namespace Recommendation {
|
||||||
|
|
||||||
std::unique_ptr<IEngine> createClustersEngine(Database::Db& db)
|
using namespace Database;
|
||||||
|
|
||||||
|
std::unique_ptr<IEngine> createClustersEngine(Db& db)
|
||||||
{
|
{
|
||||||
return std::make_unique<ClusterEngine>(db);
|
return std::make_unique<ClusterEngine>(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
ClusterEngine::getSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
ClusterEngine::findSimilarTracks(const std::vector<TrackId>& trackIds, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
Database::Session& dbSession {_db.getTLSSession()};
|
Session& dbSession {_db.getTLSSession()};
|
||||||
|
|
||||||
TrackContainer res;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
auto transaction {dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)};
|
const auto similarTrackIds {Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount})};
|
||||||
res.reserve(tracks.size());
|
return std::move(similarTrackIds.results);
|
||||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
ClusterEngine::getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const
|
ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
Database::Session& dbSession {_db.getTLSSession()};
|
Session& dbSession {_db.getTLSSession()};
|
||||||
|
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
auto transaction {dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::TrackList::pointer trackList {Database::TrackList::getById(dbSession, tracklistId)};
|
const TrackList::pointer trackList {TrackList::find(dbSession, tracklistId)};
|
||||||
if (!trackList)
|
if (!trackList)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
@@ -75,15 +70,15 @@ ClusterEngine::getSimilarTracksFromTrackList(Database::TrackListId tracklistId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReleaseContainer
|
ReleaseContainer
|
||||||
ClusterEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
|
ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
Database::Session& dbSession {_db.getTLSSession()};
|
Session& dbSession {_db.getTLSSession()};
|
||||||
|
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
{
|
{
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
auto transaction {dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
auto release {Database::Release::getById(dbSession, releaseId)};
|
auto release {Release::find(dbSession, releaseId)};
|
||||||
if (!release)
|
if (!release)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
@@ -96,24 +91,18 @@ ClusterEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t max
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArtistContainer
|
ArtistContainer
|
||||||
ClusterEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
|
ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> artistLinkTypes, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
Database::Session& dbSession {_db.getTLSSession()};
|
Session& dbSession {_db.getTLSSession()};
|
||||||
|
|
||||||
ResultContainer<Database::ArtistId> res;
|
|
||||||
{
|
|
||||||
auto transaction {dbSession.createSharedTransaction()};
|
auto transaction {dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
auto artist {Database::Artist::getById(dbSession, artistId)};
|
auto artist {Artist::find(dbSession, artistId)};
|
||||||
if (!artist)
|
if (!artist)
|
||||||
return res;
|
return {};
|
||||||
|
|
||||||
const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
|
const auto similarArtistIds {artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount})};
|
||||||
res.reserve(artists.size());
|
return std::move(similarArtistIds.results);
|
||||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Recommendation
|
} // namespace Recommendation
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ namespace Recommendation
|
|||||||
void load(bool, const ProgressCallback&) override {}
|
void load(bool, const ProgressCallback&) override {}
|
||||||
void requestCancelLoad() override {}
|
void requestCancelLoad() override {}
|
||||||
|
|
||||||
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||||
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||||
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||||
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@
|
|||||||
|
|
||||||
namespace Recommendation {
|
namespace Recommendation {
|
||||||
|
|
||||||
std::unique_ptr<IEngine> createFeaturesEngine(Database::Db& db)
|
using namespace Database;
|
||||||
|
|
||||||
|
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
|
||||||
{
|
{
|
||||||
return std::make_unique<FeaturesEngine>(db);
|
return std::make_unique<FeaturesEngine>(db);
|
||||||
}
|
}
|
||||||
@@ -56,37 +58,6 @@ FeaturesEngine::getDefaultTrainFeatureSettings()
|
|||||||
return defaultTrainFeatureSettings;
|
return defaultTrainFeatureSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
|
||||||
std::optional<FeatureValuesMap>
|
|
||||||
getTrackFeatureValues(FeaturesEngine::FeaturesFetchFunc func, Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
|
||||||
{
|
|
||||||
return func(trackId, featureNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
std::optional<FeatureValuesMap>
|
|
||||||
getTrackFeatureValuesFromDb(Database::Session& session, Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
|
||||||
{
|
|
||||||
auto func = [&](Database::TrackId trackId, const std::unordered_set<FeatureName>& featureNames)
|
|
||||||
{
|
|
||||||
std::optional<FeatureValuesMap> res;
|
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
|
||||||
|
|
||||||
Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
|
||||||
if (!track)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
res = track->getTrackFeatures()->getFeatureValuesMap(featureNames);
|
|
||||||
if (res->empty())
|
|
||||||
res.reset();
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
return getTrackFeatureValues(func, trackId, featureNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
static
|
||||||
std::optional<SOM::InputVector>
|
std::optional<SOM::InputVector>
|
||||||
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
||||||
@@ -142,45 +113,46 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr
|
|||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
std::vector<Database::TrackId> trackIds;
|
RangeResults<TrackFeaturesId> trackFeaturesIds;
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
|
||||||
trackIds = Database::Track::getAllIdsWithFeatures(session);
|
trackFeaturesIds = TrackFeatures::find(session, Range {});
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<SOM::InputVector> samples;
|
std::vector<SOM::InputVector> samples;
|
||||||
std::vector<Database::TrackId> samplesTrackIds;
|
std::vector<TrackId> samplesTrackIds;
|
||||||
|
|
||||||
samples.reserve(trackIds.size());
|
samples.reserve(trackFeaturesIds.results.size());
|
||||||
samplesTrackIds.reserve(trackIds.size());
|
samplesTrackIds.reserve(trackFeaturesIds.results.size());
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
||||||
for (Database::TrackId trackId : trackIds)
|
// TODO handle errors using exceptions
|
||||||
|
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
|
||||||
{
|
{
|
||||||
if (_loadCancelled)
|
if (_loadCancelled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::optional<FeatureValuesMap> featureValuesMap;
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
if (_featuresFetchFunc)
|
TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)};
|
||||||
featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames);
|
if (!trackFeatures)
|
||||||
else
|
|
||||||
featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames);
|
|
||||||
|
|
||||||
if (!featureValuesMap)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)};
|
FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)};
|
||||||
|
if (featureValuesMap.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)};
|
||||||
if (!inputVector)
|
if (!inputVector)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
samples.emplace_back(std::move(*inputVector));
|
samples.emplace_back(std::move(*inputVector));
|
||||||
samplesTrackIds.emplace_back(trackId);
|
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
|
||||||
}
|
}
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
|
||||||
|
|
||||||
@@ -249,41 +221,41 @@ FeaturesEngine::loadFromCache(FeaturesEngineCache cache)
|
|||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
FeaturesEngine::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const
|
FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
const TrackContainer trackIds {[&]
|
const TrackContainer trackIds {[&]
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)};
|
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
|
||||||
if (trackList)
|
if (trackList)
|
||||||
res = trackList->getTrackIds();
|
res = trackList->getTrackIds();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}()};
|
}()};
|
||||||
|
|
||||||
return getSimilarTracks(trackIds, maxCount);
|
return findSimilarTracks(trackIds, maxCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer
|
||||||
FeaturesEngine::getSimilarTracks(const std::vector<Database::TrackId>& tracksIds, std::size_t maxCount) const
|
FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
{
|
{
|
||||||
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||||
[&](Database::TrackId trackId)
|
[&](TrackId trackId)
|
||||||
{
|
{
|
||||||
return !Database::Track::exists(session, trackId);
|
return !Track::exists(session, trackId);
|
||||||
}), std::end(similarTrackIds));
|
}), std::end(similarTrackIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,11 +263,11 @@ FeaturesEngine::getSimilarTracks(const std::vector<Database::TrackId>& tracksIds
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReleaseContainer
|
ReleaseContainer
|
||||||
FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
|
FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
|
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
if (!similarReleaseIds.empty())
|
if (!similarReleaseIds.empty())
|
||||||
{
|
{
|
||||||
@@ -303,9 +275,9 @@ FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t ma
|
|||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||||
[&](Database::ReleaseId releaseId)
|
[&](ReleaseId releaseId)
|
||||||
{
|
{
|
||||||
return !Database::Release::exists(session, releaseId);
|
return !Release::exists(session, releaseId);
|
||||||
}), std::end(similarReleaseIds));
|
}), std::end(similarReleaseIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,9 +285,9 @@ FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t ma
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArtistContainer
|
ArtistContainer
|
||||||
FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||||
{
|
{
|
||||||
auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType)
|
auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType)
|
||||||
{
|
{
|
||||||
ArtistContainer similarArtistIds;
|
ArtistContainer similarArtistIds;
|
||||||
|
|
||||||
@@ -328,9 +300,9 @@ FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database:
|
|||||||
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
std::unordered_set<Database::ArtistId> similarArtistIds;
|
std::unordered_set<ArtistId> similarArtistIds;
|
||||||
|
|
||||||
for (Database::TrackArtistLinkType linkType : linkTypes)
|
for (TrackArtistLinkType linkType : linkTypes)
|
||||||
{
|
{
|
||||||
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
|
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
|
||||||
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
||||||
@@ -338,15 +310,15 @@ FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database:
|
|||||||
|
|
||||||
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
{
|
{
|
||||||
// Report only existing ids
|
// Report only existing ids
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||||
[&](Database::ArtistId artistId)
|
[&](ArtistId artistId)
|
||||||
{
|
{
|
||||||
return !Database::Artist::exists(session, artistId);
|
return !Artist::exists(session, artistId);
|
||||||
}), std::end(res));
|
}), std::end(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +378,7 @@ FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPos
|
|||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
||||||
|
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
for (const auto& [trackId, positions] : trackPositions)
|
for (const auto& [trackId, positions] : trackPositions)
|
||||||
{
|
{
|
||||||
@@ -415,7 +387,7 @@ FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPos
|
|||||||
|
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Track::pointer track {Database::Track::getById(session, trackId)};
|
const Track::pointer track {Track::find(session, trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -52,19 +52,14 @@ class FeaturesEngine : public IEngine
|
|||||||
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
|
FeaturesEngine& operator=(const FeaturesEngine&) = delete;
|
||||||
FeaturesEngine& operator=(FeaturesEngine&&) = delete;
|
FeaturesEngine& operator=(FeaturesEngine&&) = delete;
|
||||||
|
|
||||||
using FeaturesFetchFunc = std::function<std::optional<std::unordered_map<std::string, std::vector<double>>>(Database::TrackId, const std::unordered_set<std::string>& /*features*/)>;
|
|
||||||
// Default is to retrieve the features from the database (may be slow).
|
|
||||||
// Use this only if you want to train different searchers with some cached data
|
|
||||||
static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; }
|
|
||||||
|
|
||||||
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||||
void requestCancelLoad() override;
|
void requestCancelLoad() override;
|
||||||
|
|
||||||
TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||||
TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||||
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||||
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||||
|
|
||||||
@@ -121,8 +116,6 @@ class FeaturesEngine : public IEngine
|
|||||||
|
|
||||||
TrackPositions _trackPositions;
|
TrackPositions _trackPositions;
|
||||||
TrackMatrix _trackMatrix;
|
TrackMatrix _trackMatrix;
|
||||||
|
|
||||||
static inline FeaturesFetchFunc _featuresFetchFunc;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename IdType>
|
template <typename IdType>
|
||||||
|
|||||||
@@ -21,9 +21,8 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/TrackId.hpp"
|
||||||
#include "som/Network.hpp"
|
#include "som/Network.hpp"
|
||||||
|
|
||||||
namespace Recommendation {
|
namespace Recommendation {
|
||||||
|
|||||||
+4
-3
@@ -20,8 +20,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string_view>
|
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
|
#include "services/database/TrackListId.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
#include "services/recommendation/Types.hpp"
|
#include "services/recommendation/Types.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
@@ -39,8 +40,8 @@ namespace Recommendation
|
|||||||
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
||||||
virtual void cancelLoad() = 0; // wait for cancel done
|
virtual void cancelLoad() = 0; // wait for cancel done
|
||||||
|
|
||||||
virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
||||||
virtual TrackContainer getSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
virtual TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
||||||
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
||||||
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
|
|
||||||
namespace Recommendation
|
namespace Recommendation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artist
|
|||||||
// First try to get by MBID
|
// First try to get by MBID
|
||||||
if (artistInfo.musicBrainzArtistID)
|
if (artistInfo.musicBrainzArtistID)
|
||||||
{
|
{
|
||||||
artist = Artist::getByMBID(session, *artistInfo.musicBrainzArtistID);
|
artist = Artist::find(session, *artistInfo.musicBrainzArtistID);
|
||||||
if (!artist)
|
if (!artist)
|
||||||
artist = createArtist(session, artistInfo);
|
artist = createArtist(session, artistInfo);
|
||||||
else
|
else
|
||||||
@@ -151,7 +151,7 @@ getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artist
|
|||||||
// Fall back on artist name (collisions may occur)
|
// Fall back on artist name (collisions may occur)
|
||||||
if (!artistInfo.name.empty())
|
if (!artistInfo.name.empty())
|
||||||
{
|
{
|
||||||
for (const Artist::pointer& sameNamedArtist : Artist::getByName(session, artistInfo.name))
|
for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name))
|
||||||
{
|
{
|
||||||
// Do not fallback on artist that is correctly tagged
|
// Do not fallback on artist that is correctly tagged
|
||||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||||
@@ -183,7 +183,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album)
|
|||||||
// First try to get by MBID
|
// First try to get by MBID
|
||||||
if (album.musicBrainzAlbumID)
|
if (album.musicBrainzAlbumID)
|
||||||
{
|
{
|
||||||
release = Release::getByMBID(session, *album.musicBrainzAlbumID);
|
release = Release::find(session, *album.musicBrainzAlbumID);
|
||||||
if (!release)
|
if (!release)
|
||||||
{
|
{
|
||||||
release = Release::create(session, album.name, album.musicBrainzAlbumID);
|
release = Release::create(session, album.name, album.musicBrainzAlbumID);
|
||||||
@@ -200,7 +200,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album)
|
|||||||
// Fall back on release name (collisions may occur)
|
// Fall back on release name (collisions may occur)
|
||||||
if (!album.name.empty())
|
if (!album.name.empty())
|
||||||
{
|
{
|
||||||
for (const Release::pointer& sameNamedRelease : Release::getByName(session, album.name))
|
for (const Release::pointer& sameNamedRelease : Release::find(session, album.name))
|
||||||
{
|
{
|
||||||
// do not fallback on properly tagged releases
|
// do not fallback on properly tagged releases
|
||||||
if (!sameNamedRelease->getMBID())
|
if (!sameNamedRelease->getMBID())
|
||||||
@@ -227,7 +227,7 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
|
|||||||
|
|
||||||
for (auto clusterNames : clustersNames)
|
for (auto clusterNames : clustersNames)
|
||||||
{
|
{
|
||||||
auto clusterType = ClusterType::getByName(session, clusterNames.first);
|
auto clusterType = ClusterType::find(session, clusterNames.first);
|
||||||
if (!clusterType)
|
if (!clusterType)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -249,12 +249,12 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
|
|||||||
namespace Scanner {
|
namespace Scanner {
|
||||||
|
|
||||||
std::unique_ptr<IScannerService>
|
std::unique_ptr<IScannerService>
|
||||||
createScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService)
|
createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||||
{
|
{
|
||||||
return std::make_unique<ScannerService>(db, recommendationService);
|
return std::make_unique<ScannerService>(db, recommendationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScannerService::ScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService)
|
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
||||||
: _recommendationService {recommendationService}
|
: _recommendationService {recommendationService}
|
||||||
, _dbSession {db}
|
, _dbSession {db}
|
||||||
{
|
{
|
||||||
@@ -543,7 +543,7 @@ ScannerService::scan(bool forceScan)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
ScannerService::fetchTrackFeatures(Database::TrackId trackId, const UUID& recordingMBID)
|
ScannerService::fetchTrackFeatures(TrackId trackId, const UUID& recordingMBID)
|
||||||
{
|
{
|
||||||
std::map<std::string, double> features;
|
std::map<std::string, double> features;
|
||||||
|
|
||||||
@@ -558,11 +558,11 @@ ScannerService::fetchTrackFeatures(Database::TrackId trackId, const UUID& record
|
|||||||
{
|
{
|
||||||
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::Track::pointer track {Database::Track::getById(_dbSession, trackId)};
|
Track::pointer track {Track::find(_dbSession, trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Database::TrackFeatures::create(_dbSession, track, data);
|
TrackFeatures::create(_dbSession, track, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -580,7 +580,7 @@ ScannerService::fetchTrackFeatures(ScanStats& stats)
|
|||||||
|
|
||||||
struct TrackInfo
|
struct TrackInfo
|
||||||
{
|
{
|
||||||
Database::TrackId id;
|
TrackId id;
|
||||||
UUID recordingMBID;
|
UUID recordingMBID;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -590,9 +590,12 @@ ScannerService::fetchTrackFeatures(ScanStats& stats)
|
|||||||
|
|
||||||
auto transaction {_dbSession.createSharedTransaction()};
|
auto transaction {_dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
auto tracks {Database::Track::getAllWithRecordingMBIDAndMissingFeatures(_dbSession)};
|
auto trackIds {Track::findWithRecordingMBIDAndMissingFeatures(_dbSession, Range {})};
|
||||||
for (const auto& track : tracks)
|
for (const TrackId trackId : trackIds.results)
|
||||||
|
{
|
||||||
|
const Track::pointer track {Track::find(_dbSession, trackId)};
|
||||||
res.emplace_back(TrackInfo {track->getId(), *track->getRecordingMBID()});
|
res.emplace_back(TrackInfo {track->getId(), *track->getRecordingMBID()});
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}()};
|
}()};
|
||||||
@@ -692,7 +695,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
|
|||||||
// Skip file if last write is the same
|
// Skip file if last write is the same
|
||||||
auto transaction {_dbSession.createSharedTransaction()};
|
auto transaction {_dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
const Track::pointer track {Track::getByPath(_dbSession, file)};
|
const Track::pointer track {Track::findByPath(_dbSession, file)};
|
||||||
|
|
||||||
if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t()
|
if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t()
|
||||||
&& track->getScanVersion() == _scanVersion)
|
&& track->getScanVersion() == _scanVersion)
|
||||||
@@ -713,7 +716,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
|
|||||||
|
|
||||||
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
||||||
|
|
||||||
Track::pointer track {Track::getByPath(_dbSession, file) };
|
Track::pointer track {Track::findByPath(_dbSession, file) };
|
||||||
|
|
||||||
// We estimate this is an audio file if:
|
// We estimate this is an audio file if:
|
||||||
// - we found a least one audio stream
|
// - we found a least one audio stream
|
||||||
@@ -778,30 +781,30 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
|
|||||||
track.modify()->clearArtistLinks();
|
track.modify()->clearArtistLinks();
|
||||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||||
for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists, false))
|
for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists, false))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, artist, TrackArtistLinkType::Artist));
|
||||||
|
|
||||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists, false))
|
for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists, false))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
||||||
|
|
||||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||||
for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists, true))
|
for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
||||||
|
|
||||||
for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists, true))
|
for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, composer, TrackArtistLinkType::Composer));
|
||||||
|
|
||||||
for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists, true))
|
for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
||||||
|
|
||||||
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
|
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
||||||
|
|
||||||
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
|
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer));
|
||||||
|
|
||||||
for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists, true))
|
for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists, true))
|
||||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
||||||
|
|
||||||
track.modify()->setScanVersion(_scanVersion);
|
track.modify()->setScanVersion(_scanVersion);
|
||||||
if (trackInfo->album)
|
if (trackInfo->album)
|
||||||
@@ -827,7 +830,8 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
|
|||||||
|
|
||||||
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
||||||
track.modify()->setTrackMBID(trackInfo->trackMBID);
|
track.modify()->setTrackMBID(trackInfo->trackMBID);
|
||||||
track.modify()->setFeatures({}); // TODO: only if MBID changed?
|
if (auto trackFeatures {TrackFeatures::find(_dbSession, track->getId())})
|
||||||
|
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||||
track.modify()->setHasCover(trackInfo->hasCover);
|
track.modify()->setHasCover(trackInfo->hasCover);
|
||||||
track.modify()->setCopyright(trackInfo->copyright);
|
track.modify()->setCopyright(trackInfo->copyright);
|
||||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||||
@@ -921,26 +925,25 @@ ScannerService::removeMissingTracks(ScanStats& stats)
|
|||||||
stepStats.totalElems = trackCount;
|
stepStats.totalElems = trackCount;
|
||||||
notifyInProgress(stepStats);
|
notifyInProgress(stepStats);
|
||||||
|
|
||||||
std::vector<std::pair<Database::TrackId, std::filesystem::path>> trackPaths;
|
RangeResults<Track::PathResult> trackPaths;
|
||||||
std::vector<TrackId> tracksToRemove;
|
std::vector<TrackId> tracksToRemove;
|
||||||
|
|
||||||
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
|
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
|
||||||
{
|
{
|
||||||
trackPaths.clear();
|
|
||||||
tracksToRemove.clear();
|
tracksToRemove.clear();
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {_dbSession.createSharedTransaction()};
|
auto transaction {_dbSession.createSharedTransaction()};
|
||||||
trackPaths = Track::getAllPaths(_dbSession, i, batchSize);
|
trackPaths = Track::findPaths(_dbSession, Range {i, batchSize});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [trackId, trackPath] : trackPaths)
|
for (const Track::PathResult& trackPath : trackPaths.results)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
|
if (!checkFile(trackPath.path, _mediaDirectory, _fileExtensions))
|
||||||
tracksToRemove.push_back(trackId);
|
tracksToRemove.push_back(trackPath.trackId);
|
||||||
|
|
||||||
stepStats.processedElems++;
|
stepStats.processedElems++;
|
||||||
}
|
}
|
||||||
@@ -951,7 +954,7 @@ ScannerService::removeMissingTracks(ScanStats& stats)
|
|||||||
|
|
||||||
for (const TrackId trackId : tracksToRemove)
|
for (const TrackId trackId : tracksToRemove)
|
||||||
{
|
{
|
||||||
Track::pointer track {Track::getById(_dbSession, trackId)};
|
Track::pointer track {Track::find(_dbSession, trackId)};
|
||||||
if (track)
|
if (track)
|
||||||
{
|
{
|
||||||
track.remove();
|
track.remove();
|
||||||
@@ -977,9 +980,10 @@ ScannerService::removeOrphanEntries()
|
|||||||
auto transaction {_dbSession.createUniqueTransaction()};
|
auto transaction {_dbSession.createUniqueTransaction()};
|
||||||
|
|
||||||
// Now process orphan Cluster (no track)
|
// Now process orphan Cluster (no track)
|
||||||
auto clusters {Cluster::getAllOrphans(_dbSession)};
|
auto clusterIds {Cluster::findOrphans(_dbSession, Range {})};
|
||||||
for (auto& cluster : clusters)
|
for (ClusterId clusterId : clusterIds.results)
|
||||||
{
|
{
|
||||||
|
Cluster::pointer cluster {Cluster::find(_dbSession, clusterId)};
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
|
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
|
||||||
cluster.remove();
|
cluster.remove();
|
||||||
}
|
}
|
||||||
@@ -989,9 +993,10 @@ ScannerService::removeOrphanEntries()
|
|||||||
{
|
{
|
||||||
auto transaction {_dbSession.createUniqueTransaction()};
|
auto transaction {_dbSession.createUniqueTransaction()};
|
||||||
|
|
||||||
auto artists {Artist::getAllOrphans(_dbSession)};
|
auto artistIds {Artist::findAllOrphans(_dbSession, Range {})};
|
||||||
for (auto& artist : artists)
|
for (const ArtistId artistId : artistIds.results)
|
||||||
{
|
{
|
||||||
|
Artist::pointer artist {Artist::find(_dbSession, artistId)};
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
|
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
|
||||||
artist.remove();
|
artist.remove();
|
||||||
}
|
}
|
||||||
@@ -1001,9 +1006,10 @@ ScannerService::removeOrphanEntries()
|
|||||||
{
|
{
|
||||||
auto transaction {_dbSession.createUniqueTransaction()};
|
auto transaction {_dbSession.createUniqueTransaction()};
|
||||||
|
|
||||||
auto releases {Release::getAllOrphans(_dbSession)};
|
auto releases {Release::findOrphans(_dbSession, Range {})};
|
||||||
for (auto& release : releases)
|
for (const ReleaseId releaseId : releases.results)
|
||||||
{
|
{
|
||||||
|
Release::pointer release {Release::find(_dbSession, releaseId)};
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
|
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
|
||||||
release.remove();
|
release.remove();
|
||||||
}
|
}
|
||||||
@@ -1019,9 +1025,10 @@ ScannerService::checkDuplicatedAudioFiles(ScanStats& stats)
|
|||||||
|
|
||||||
auto transaction {_dbSession.createSharedTransaction()};
|
auto transaction {_dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
const std::vector<Track::pointer> tracks = Database::Track::getMBIDDuplicates(_dbSession);
|
const RangeResults<TrackId> tracks = Track::findMBIDDuplicates(_dbSession, Range {});
|
||||||
for (const Track::pointer& track : tracks)
|
for (const TrackId trackId : tracks.results)
|
||||||
{
|
{
|
||||||
|
const Track::pointer track {Track::find(_dbSession, trackId)};
|
||||||
if (auto trackMBID {track->getTrackMBID()})
|
if (auto trackMBID {track->getTrackMBID()})
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, INFO) << "Found duplicated Track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
|
LMS_LOG(DBUPDATER, INFO) << "Found duplicated Track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/TrackId.hpp"
|
||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,8 +23,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/TrackListId.hpp"
|
||||||
#include "services/scrobbling/Listen.hpp"
|
#include "services/scrobbling/Listen.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
@@ -36,18 +37,26 @@ namespace Database
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
|
|
||||||
class IScrobbler
|
class IScrobbler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IScrobbler() = default;
|
virtual ~IScrobbler() = default;
|
||||||
|
|
||||||
|
// Listens
|
||||||
virtual void listenStarted(const Listen& listen) = 0;
|
virtual void listenStarted(const Listen& listen) = 0;
|
||||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
||||||
|
|
||||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||||
|
|
||||||
virtual Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) = 0;
|
// Feedback
|
||||||
|
virtual void onStarred(Database::UserId, Database::ArtistId) {};
|
||||||
|
virtual void onUnstarred(Database::UserId, Database::ArtistId) {};
|
||||||
|
virtual void onStarred(Database::UserId, Database::ReleaseId) {};
|
||||||
|
virtual void onUnstarred(Database::UserId, Database::ReleaseId) {};
|
||||||
|
virtual void onStarred(Database::UserId, Database::TrackId) {};
|
||||||
|
virtual void onUnstarred(Database::UserId, Database::TrackId) {};
|
||||||
|
|
||||||
|
// virtual void star(Database::TrackId trackId) = 0;
|
||||||
|
// virtual void unstar(Database::TrackId trackId) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include "services/scrobbling/Listen.hpp"
|
||||||
|
|
||||||
|
namespace Scrobbling
|
||||||
|
{
|
||||||
|
ScrobblerBase::ScrobblerBase(Database::Db& db)
|
||||||
|
: _db {db}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::TrackListId
|
||||||
|
ScrobberBase::getListensTrackList(Database::UserId userId, std::string_view trackListName)
|
||||||
|
{
|
||||||
|
std::string_view listensTrackListName {getListensTrackListName()};
|
||||||
|
Database::Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
if (auto trackList {Database::TrackList::get(session, listensTrackListName, Database::TrackList::Type::Internal, userId)})
|
||||||
|
return trackList->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
if (auto trackList {Database::TrackList::get(session, listensTrackListName, Database::TrackList::Type::Internal, userId)})
|
||||||
|
return trackList->getId();
|
||||||
|
|
||||||
|
const Database::User::pointer user {Database::User::getById(session, userId)};
|
||||||
|
if (!user)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Database::TrackList::pointer trackList {Database::TrackList::create(session, listensTrackListName, Database::TrackList::Type::Internal, false, user)};
|
||||||
|
return trackList->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ScrobberBase::saveTimedListen(const TimedListen& listen, std::string_view trackListName)
|
||||||
|
{
|
||||||
|
const Database::TrackListId trackListId {getListensTrackList(listen.userId)};
|
||||||
|
if (!trackListId)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // ns Scrobbling
|
||||||
|
|
||||||
@@ -18,13 +18,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ScrobblingService.hpp"
|
#include "ScrobblingService.hpp"
|
||||||
|
#include "ScrobblingService.impl.hpp"
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/StarredArtist.hpp"
|
||||||
|
#include "services/database/StarredRelease.hpp"
|
||||||
|
#include "services/database/StarredTrack.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
#include "internal/InternalScrobbler.hpp"
|
#include "internal/InternalScrobbler.hpp"
|
||||||
@@ -43,202 +47,248 @@ namespace Scrobbling
|
|||||||
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||||
: _db {db}
|
: _db {db}
|
||||||
{
|
{
|
||||||
_scrobblers.emplace(Database::Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||||
_scrobblers.emplace(Database::Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ScrobblingService::listenStarted(const Listen& listen)
|
ScrobblingService::listenStarted(const Listen& listen)
|
||||||
{
|
{
|
||||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
_scrobblers[*scrobbler]->listenStarted(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||||
{
|
{
|
||||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ScrobblingService::addTimedListen(const TimedListen& listen)
|
ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||||
{
|
{
|
||||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
_scrobblers[*scrobbler]->addTimedListen(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Database::Scrobbler>
|
std::optional<Scrobbler>
|
||||||
ScrobblingService::getUserScrobbler(Database::UserId userId)
|
ScrobblingService::getUserScrobbler(UserId userId)
|
||||||
{
|
{
|
||||||
std::optional<Database::Scrobbler> scrobbler;
|
std::optional<Scrobbler> scrobbler;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
if (const User::pointer user {User::getById(session, userId)})
|
if (const User::pointer user {User::find(session, userId)})
|
||||||
scrobbler = user->getScrobbler();
|
scrobbler = user->getScrobbler();
|
||||||
|
|
||||||
return scrobbler;
|
return scrobbler;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ArtistContainer
|
ScrobblingService::ArtistContainer
|
||||||
ScrobblingService::getRecentArtists(UserId userId,
|
ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
std::optional<TrackArtistLinkType> linkType,
|
|
||||||
std::optional<Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
ArtistContainer res;
|
ArtistContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||||
if (!user)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
const ObjectPtr<TrackList> history {getListensTrackList(session, user)};
|
|
||||||
if (history)
|
|
||||||
{
|
|
||||||
for (const Artist::pointer& artist : history->getArtistsReverse(clusterIds, linkType, range, moreResults))
|
|
||||||
res.push_back(artist->getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer
|
ScrobblingService::ReleaseContainer
|
||||||
ScrobblingService::getRecentReleases(Database::UserId userId,
|
ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
||||||
if (!user)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
const ObjectPtr<TrackList> history {getListensTrackList(session, user)};
|
|
||||||
if (history)
|
|
||||||
{
|
|
||||||
for (const Release::pointer& release : history->getReleasesReverse(clusterIds, range, moreResults))
|
|
||||||
res.push_back(release->getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::TrackContainer
|
ScrobblingService::TrackContainer
|
||||||
ScrobblingService::getRecentTracks(Database::UserId userId,
|
ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
||||||
if (!user)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
const ObjectPtr<TrackList> history {getListensTrackList(session, user)};
|
|
||||||
if (history)
|
|
||||||
{
|
|
||||||
for (const Track::pointer& track : history->getTracksReverse(clusterIds, range, moreResults))
|
|
||||||
res.push_back(track->getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
ScrobblingService::ArtistContainer
|
ScrobblingService::ArtistContainer
|
||||||
ScrobblingService::getTopArtists(UserId userId,
|
ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
|
||||||
std::optional<Database::Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
ArtistContainer res;
|
ArtistContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||||
if (!user)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
const ObjectPtr<TrackList> history {getListensTrackList(session, user)};
|
|
||||||
if (history)
|
|
||||||
{
|
|
||||||
for (const Artist::pointer& artist : history->getTopArtists(clusterIds, linkType, range, moreResults))
|
|
||||||
res.push_back(artist->getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer
|
ScrobblingService::ReleaseContainer
|
||||||
ScrobblingService::getTopReleases(Database::UserId userId,
|
ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
||||||
if (!user)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
const ObjectPtr<TrackList> history {getListensTrackList(session, user)};
|
|
||||||
if (history)
|
|
||||||
{
|
|
||||||
for (const Release::pointer& release : history->getTopReleases(clusterIds, range, moreResults))
|
|
||||||
res.push_back(release->getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::TrackContainer
|
ScrobblingService::TrackContainer
|
||||||
ScrobblingService::getTopTracks(Database::UserId userId,
|
ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::Range> range,
|
|
||||||
bool& moreResults)
|
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const User::pointer user {User::getById(session, userId)};
|
return Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
||||||
if (!user)
|
}
|
||||||
return res;
|
|
||||||
|
|
||||||
if (const ObjectPtr<TrackList> history {getListensTrackList(session, user)})
|
void
|
||||||
|
ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||||
{
|
{
|
||||||
for (const Track::pointer& track : history->getTopTracks(clusterIds, range, moreResults))
|
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
res.push_back(track->getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
void
|
||||||
}
|
ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||||
|
|
||||||
Database::ObjectPtr<Database::TrackList>
|
|
||||||
ScrobblingService::getListensTrackList(Session& session, Database::ObjectPtr<Database::User> user)
|
|
||||||
{
|
{
|
||||||
return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user);
|
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
||||||
|
{
|
||||||
|
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrobblingService::ArtistContainer
|
||||||
|
ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
||||||
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
|
ArtistSortMethod sortMethod,
|
||||||
|
Range range)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Artist::FindParameters params;
|
||||||
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setLinkType(linkType);
|
||||||
|
params.setSortMethod(sortMethod);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
return Artist::find(session, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrobblingService::ReleaseContainer
|
||||||
|
ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
return Release::find(session, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ScrobblingService::star(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrobblingService::TrackContainer
|
||||||
|
ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setSortMethod(TrackSortMethod::StarredDateDesc);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
return Track::find(session, params);
|
||||||
|
}
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -41,39 +41,57 @@ namespace Scrobbling
|
|||||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
ArtistContainer getRecentArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
ReleaseContainer getRecentReleases(Database::UserId userId,
|
ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
TrackContainer getRecentTracks(Database::UserId userId,
|
TrackContainer getRecentTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
ArtistContainer getTopArtists(Database::UserId userId,
|
ArtistContainer getTopArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
ReleaseContainer getTopReleases(Database::UserId userId,
|
ReleaseContainer getTopReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
TrackContainer getTopTracks(Database::UserId userId,
|
TrackContainer getTopTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::Range> range,
|
Database::Range range) override;
|
||||||
bool& moreResults) override;
|
|
||||||
|
|
||||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
void star(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
|
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
|
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
|
ArtistContainer getStarredArtists(Database::UserId userId,
|
||||||
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
|
Database::ArtistSortMethod sortMethod,
|
||||||
|
Database::Range range) override;
|
||||||
|
|
||||||
|
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
|
||||||
|
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
|
||||||
|
bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override;
|
||||||
|
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
|
void star(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
|
void unstar(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
|
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
|
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void star(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void unstar(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
bool isStarred(Database::UserId userId, ObjIdType id);
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 "services/database/Db.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
namespace Scrobbling
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void
|
||||||
|
ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||||
|
if (!starredObj)
|
||||||
|
{
|
||||||
|
const typename ObjType::pointer obj {ObjType::find(session, objId)};
|
||||||
|
if (!obj)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const User::pointer user {User::find(session, userId)};
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
|
||||||
|
starredObj = StarredObjType::create(session, obj, user, *scrobbler);
|
||||||
|
}
|
||||||
|
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||||
|
}
|
||||||
|
_scrobblers[*scrobbler]->onStarred(userId, objId);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void
|
||||||
|
ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
if (typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)})
|
||||||
|
starredObj.remove();
|
||||||
|
}
|
||||||
|
_scrobblers[*scrobbler]->onUnstarred(userId, objId);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
bool
|
||||||
|
ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
return StarredObjType::find(session, objId, userId, *scrobbler);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // ns Scrobbling
|
||||||
|
|
||||||
@@ -20,22 +20,19 @@
|
|||||||
#include "InternalScrobbler.hpp"
|
#include "InternalScrobbler.hpp"
|
||||||
|
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
|
||||||
#include "services/database/TrackList.hpp"
|
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "services/database/Track.hpp"
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
static const std::string historyTracklistName {"__scrobbler_internal_history__"};
|
|
||||||
|
|
||||||
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
||||||
: _db {db}
|
: _db {db}
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void
|
void
|
||||||
InternalScrobbler::listenStarted(const Listen& /*listen*/)
|
InternalScrobbler::listenStarted(const Listen&)
|
||||||
{
|
{
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
@@ -43,7 +40,7 @@ namespace Scrobbling
|
|||||||
void
|
void
|
||||||
InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||||
{
|
{
|
||||||
// record tracks that have been played for at least of few seconds...
|
// only record tracks that have been played for at least of few seconds...
|
||||||
if (duration && *duration < std::chrono::seconds {5})
|
if (duration && *duration < std::chrono::seconds {5})
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -54,29 +51,20 @@ namespace Scrobbling
|
|||||||
InternalScrobbler::addTimedListen(const TimedListen& listen)
|
InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||||
{
|
{
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Database::Session& session {_db.getTLSSession()};
|
||||||
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::getById(session, listen.userId)};
|
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const Database::User::pointer user {Database::User::find(session, listen.userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Database::TrackList::pointer tracklist {getListensTrackList(session, user)};
|
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
||||||
if (!tracklist)
|
|
||||||
tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user);
|
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
|
||||||
if (!track)
|
if (!track)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Database::TrackListEntry::create(session, track, getListensTrackList(session, user), listen.listenedAt);
|
Database::Listen::create(session, user, track, Database::Scrobbler::Internal, listen.listenedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::TrackList::pointer
|
|
||||||
InternalScrobbler::getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user)
|
|
||||||
{
|
|
||||||
return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // Scrobbling
|
} // Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,11 @@ namespace Scrobbling
|
|||||||
InternalScrobbler(Database::Db& db);
|
InternalScrobbler(Database::Db& db);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// IScrobbler
|
||||||
void listenStarted(const Listen& listen) override;
|
void listenStarted(const Listen& listen) override;
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
|
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
|
|
||||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) override;
|
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
};
|
};
|
||||||
} // Scrobbling
|
} // Scrobbling
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
const Database::Track::pointer track {Database::Track::find(session, trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ namespace
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
@@ -179,12 +179,6 @@ namespace Scrobbling::ListenBrainz
|
|||||||
enqueListen(listen, listen.listenedAt);
|
enqueListen(listen, listen.listenedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::TrackList::pointer
|
|
||||||
Scrobbler::getListensTrackList(Database::Session& session, Database::User::pointer user)
|
|
||||||
{
|
|
||||||
return Utils::getListensTrackList(session, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
|
Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,11 +49,9 @@ namespace Scrobbling::ListenBrainz
|
|||||||
void listenStarted(const Listen& listen) override;
|
void listenStarted(const Listen& listen) override;
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user) override;
|
|
||||||
|
|
||||||
// Submit listens
|
// Submit listens
|
||||||
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
|
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
|
||||||
//std::optional<SendQueue::RequestData> createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint);
|
|
||||||
|
|
||||||
boost::asio::io_context& _ioContext;
|
boost::asio::io_context& _ioContext;
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
@@ -87,10 +88,10 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::Track::pointer
|
Database::TrackId
|
||||||
tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata)
|
tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata)
|
||||||
{
|
{
|
||||||
Database::Track::pointer track;
|
using namespace Database;
|
||||||
|
|
||||||
// first try to get the associated track using MBIDs, and then fallback on names
|
// first try to get the associated track using MBIDs, and then fallback on names
|
||||||
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
||||||
@@ -98,30 +99,29 @@ namespace
|
|||||||
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
||||||
if (std::optional<UUID> recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))})
|
if (std::optional<UUID> recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))})
|
||||||
{
|
{
|
||||||
const auto tracks {Database::Track::getByRecordingMBID(session, *recordingMBID)};
|
const auto tracks {Track::findByRecordingMBID(session, *recordingMBID)};
|
||||||
// if duplicated files, do not record it (let the user correct its database)
|
// if duplicated files, do not record it (let the user correct its database)
|
||||||
if (tracks.size() == 1)
|
if (tracks.size() == 1)
|
||||||
track = tracks.front();
|
return tracks.front()->getId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track)
|
|
||||||
return track;
|
|
||||||
|
|
||||||
// these fields are mandatory
|
// these fields are mandatory
|
||||||
const std::string trackName {static_cast<std::string>(metadata.get("track_name"))};
|
const std::string trackName {static_cast<std::string>(metadata.get("track_name"))};
|
||||||
const std::string releaseName {static_cast<std::string>(metadata.get("release_name"))};
|
const std::string releaseName {static_cast<std::string>(metadata.get("release_name"))};
|
||||||
|
|
||||||
auto tracks {Database::Track::getByNameAndReleaseName(session, trackName, releaseName)};
|
auto tracks {Track::findByNameAndReleaseName(session, trackName, releaseName)};
|
||||||
if (tracks.size() > 1)
|
if (tracks.results.size() > 1)
|
||||||
{
|
{
|
||||||
tracks.erase(std::remove_if(std::begin(tracks), std::end(tracks),
|
tracks.results.erase(std::remove_if(std::begin(tracks.results), std::end(tracks.results),
|
||||||
[&](const Database::Track::pointer track)
|
[&](const TrackId trackId)
|
||||||
{
|
{
|
||||||
|
const Track::pointer track {Track::find(session, trackId)};
|
||||||
|
|
||||||
if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty())
|
if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty())
|
||||||
{
|
{
|
||||||
const auto& artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
const auto& artists {track->getArtists({TrackArtistLinkType::Artist})};
|
||||||
if (std::none_of(std::begin(artists), std::end(artists), [&](const Database::Artist::pointer& artist) { return artist->getName() == artistName; }))
|
if (std::none_of(std::begin(artists), std::end(artists), [&](const Artist::pointer& artist) { return artist->getName() == artistName; }))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
||||||
@@ -145,13 +145,13 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}), std::end(tracks));
|
}), std::end(tracks.results));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.size() == 1)
|
if (tracks.results.size() == 1)
|
||||||
track = tracks.front();
|
return tracks.results.front();
|
||||||
|
|
||||||
return track;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParseGetListensResult
|
struct ParseGetListensResult
|
||||||
@@ -198,8 +198,8 @@ namespace
|
|||||||
else if (listenedAt < result.oldestEntry)
|
else if (listenedAt < result.oldestEntry)
|
||||||
result.oldestEntry = listenedAt;
|
result.oldestEntry = listenedAt;
|
||||||
|
|
||||||
if (const Database::Track::pointer track {tryMatchListen(session, metadata)})
|
if (Database::TrackId trackId {tryMatchListen(session, metadata)}; trackId.isValid())
|
||||||
result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt});
|
result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, trackId}, listenedAt});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const Wt::WException& error)
|
catch (const Wt::WException& error)
|
||||||
@@ -234,15 +234,18 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::getById(session, listen.userId)};
|
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const Database::User::pointer user {Database::User::find(session, listen.userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Database::TrackListEntry::create(session, track, Utils::getOrCreateListensTrackList(session, user), listen.listenedAt);
|
Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt);
|
||||||
|
|
||||||
UserContext& context {getUserContext(listen.userId)};
|
UserContext& context {getUserContext(listen.userId)};
|
||||||
if (context.listenCount)
|
if (context.listenCount)
|
||||||
@@ -304,14 +307,14 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
assert(!isFetching());
|
assert(!isFetching());
|
||||||
|
|
||||||
std::vector<Database::UserId> userIds;
|
Database::RangeResults<Database::UserId> userIds;
|
||||||
{
|
{
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Database::Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
userIds = Database::User::getAllIds(_db.getTLSSession());
|
userIds = Database::User::find(_db.getTLSSession(), Database::Range {});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const Database::UserId userId : userIds)
|
for (const Database::UserId userId : userIds.results)
|
||||||
{
|
{
|
||||||
if (Utils::getListenBrainzToken(_db.getTLSSession(), userId))
|
if (Utils::getListenBrainzToken(_db.getTLSSession(), userId))
|
||||||
startGetListens(getUserContext(userId));
|
startGetListens(getUserContext(userId));
|
||||||
@@ -458,25 +461,22 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::User::pointer user {Database::User::getById(session, context.userId)};
|
Database::User::pointer user {Database::User::find(session, context.userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Database::TrackList::pointer tracklist {Utils::getOrCreateListensTrackList(session, user)};
|
|
||||||
|
|
||||||
for (const TimedListen& listen : parseResult.matchedListens)
|
for (const TimedListen& listen : parseResult.matchedListens)
|
||||||
{
|
{
|
||||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
||||||
if (!track)
|
if (!track)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!tracklist->getEntryByTrackAndDateTime(track, listen.listenedAt))
|
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt))
|
||||||
{
|
continue;
|
||||||
context.importedListenCount++;
|
|
||||||
Database::TrackListEntry::create(session, track, tracklist, listen.listenedAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt);
|
||||||
|
context.importedListenCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace Scrobbling::ListenBrainz
|
} // namespace Scrobbling::ListenBrainz
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <boost/asio/io_context_strand.hpp>
|
#include <boost/asio/io_context_strand.hpp>
|
||||||
#include <boost/asio/steady_timer.hpp>
|
#include <boost/asio/steady_timer.hpp>
|
||||||
#include <Wt/Dbo/Dbo.h>
|
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "services/scrobbling/Listen.hpp"
|
#include "services/scrobbling/Listen.hpp"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/io_context_strand.hpp>
|
||||||
|
#include <boost/asio/steady_timer.hpp>
|
||||||
|
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "services/scrobbling/Listen.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
class Session;
|
||||||
|
class TrackList;
|
||||||
|
class User;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Scrobbling::ListenBrainz
|
||||||
|
{
|
||||||
|
class ListensSynchronizer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, std::string_view baseAPIUrl);
|
||||||
|
|
||||||
|
void saveListen(const TimedListen& listen);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct UserContext
|
||||||
|
{
|
||||||
|
UserContext(Database::UserId id) : userId {id} {}
|
||||||
|
|
||||||
|
UserContext(const UserContext&) = delete;
|
||||||
|
UserContext(UserContext&&) = delete;
|
||||||
|
UserContext& operator=(const UserContext&) = delete;
|
||||||
|
UserContext& operator=(UserContext&&) = delete;
|
||||||
|
|
||||||
|
const Database::UserId userId;
|
||||||
|
bool fetching {};
|
||||||
|
|
||||||
|
// resetted at each fetch
|
||||||
|
std::string listenBrainzUserName; // need to be resolved first
|
||||||
|
Wt::WDateTime maxDateTime;
|
||||||
|
std::size_t fetchedLoveCount{};
|
||||||
|
std::size_t matchedLoveCount{};
|
||||||
|
std::size_t importedLoveCount{};
|
||||||
|
};
|
||||||
|
|
||||||
|
UserContext& getUserContext(Database::UserId userId);
|
||||||
|
bool isFetching() const;
|
||||||
|
void scheduleGetListens(std::chrono::seconds fromNow);
|
||||||
|
void startGetListens();
|
||||||
|
void startGetListens(UserContext& context);
|
||||||
|
void onGetListensEnded(UserContext& context);
|
||||||
|
void enqueValidateToken(UserContext& context);
|
||||||
|
void enqueGetListenCount(UserContext& context);
|
||||||
|
void enqueGetListens(UserContext& context);
|
||||||
|
void processGetListensResponse(std::string_view body, UserContext& context);
|
||||||
|
|
||||||
|
boost::asio::io_context& _ioContext;
|
||||||
|
boost::asio::io_context::strand _strand {_ioContext};
|
||||||
|
Database::Db& _db;
|
||||||
|
std::string _baseAPIUrl;
|
||||||
|
boost::asio::steady_timer _getListensTimer {_ioContext};
|
||||||
|
|
||||||
|
std::unordered_map<Database::UserId, UserContext> _userContexts;
|
||||||
|
|
||||||
|
const std::size_t _maxSyncListenCount;
|
||||||
|
const std::chrono::hours _syncListensPeriod;
|
||||||
|
};
|
||||||
|
} // Scrobbling::ListenBrainz
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ namespace Scrobbling::ListenBrainz::Utils
|
|||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::getById(session, userId)};
|
const Database::User::pointer user {Database::User::find(session, userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
@@ -43,21 +43,4 @@ namespace Scrobbling::ListenBrainz::Utils
|
|||||||
|
|
||||||
return user->getListenBrainzToken();
|
return user->getListenBrainzToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::TrackList::pointer
|
|
||||||
getListensTrackList(Database::Session& session, Database::User::pointer user)
|
|
||||||
{
|
|
||||||
return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::TrackList::pointer
|
|
||||||
getOrCreateListensTrackList(Database::Session& session, Database::User::pointer user)
|
|
||||||
{
|
|
||||||
Database::TrackList::pointer tracklist {getListensTrackList(session, user)};
|
|
||||||
if (!tracklist)
|
|
||||||
tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user);
|
|
||||||
|
|
||||||
return tracklist;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user