diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 915aadd5..109c24b7 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -35,448 +35,443 @@ namespace Database { -Wt::Dbo::Query -createQuery(Session& session, const Release::FindParameters& params) -{ - auto query {session.getDboSession().query("SELECT DISTINCT r.id from release r")}; - - if (params.sortMethod == ReleaseSortMethod::LastWritten - || params.sortMethod == ReleaseSortMethod::Date - || params.sortMethod == ReleaseSortMethod::OriginalDate - || params.sortMethod == ReleaseSortMethod::OriginalDateDesc - || params.writtenAfter.isValid() - || params.dateRange - || params.artist.isValid()) - { - query.join("track t ON t.release_id = r.id"); - } - - if (params.writtenAfter.isValid()) - query.where("t.file_last_write > ?").bind(params.writtenAfter); - - if (params.dateRange) - { - query.where("t.date >= ?").bind(params.dateRange->begin); - query.where("t.date <= ?").bind(params.dateRange->end); - } - - for (std::string_view keyword : params.keywords) - query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%"); - - if (params.starringUser.isValid()) - { - assert(params.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) - .where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); - } - - if (params.artist.isValid()) - { - query.join("artist a ON a.id = t_a_l.artist_id") - .join("track_artist_link t_a_l ON t_a_l.track_id = t.id") - .where("a.id = ?").bind(params.artist); - - if (!params.trackArtistLinkTypes.empty()) - { - std::ostringstream oss; - - bool first {true}; - for (TrackArtistLinkType linkType : params.trackArtistLinkTypes) - { - if (!first) - oss << " OR "; - oss << "t_a_l.type = ?"; - query.bind(linkType); - - first = false; - } - query.where(oss.str()); - } - - if (!params.excludedTrackArtistLinkTypes.empty()) - { - std::ostringstream oss; - oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN artist a ON a.id = t_a_l.artist_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" - " INNER JOIN track t ON t.release_id = r.id" - " WHERE (a.id = ? AND ("; - - query.bind(params.artist); - - bool first {true}; - for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes) - { - if (!first) - oss << " OR "; - oss << "t_a_l.type = ?"; - query.bind(linkType); - - first = false; - } - oss << ")))"; - query.where(oss.str()); - } - } - - if (!params.clusters.empty()) - { - std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; - - WhereClause clusterClause; - for (const ClusterId clusterId : params.clusters) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(clusterId); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; - - query.where(oss.str()); - } - - if (params.primaryType) - query.where("primary_type = ?").bind(*params.primaryType); - if (!params.secondaryTypes.empty()) - query.where("secondary_type = ?").bind(params.secondaryTypes); - - switch (params.sortMethod) - { - case ReleaseSortMethod::None: - break; - case ReleaseSortMethod::Name: - query.orderBy("r.name COLLATE NOCASE"); - break; - case ReleaseSortMethod::Random: - query.orderBy("RANDOM()"); - break; - case ReleaseSortMethod::LastWritten: - query.orderBy("t.file_last_write DESC"); - break; - case ReleaseSortMethod::Date: - query.orderBy("t.date, r.name COLLATE NOCASE"); - break; - case ReleaseSortMethod::OriginalDate: - query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE"); - break; - case ReleaseSortMethod::OriginalDateDesc: - query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE"); - break; - case ReleaseSortMethod::StarredDateDesc: - assert(params.starringUser.isValid()); - query.orderBy("s_r.date_time DESC"); - break; - } - - return query; -} - -Release::Release(const std::string& name, const std::optional& MBID) -: _name {std::string(name, 0 , _maxNameLength)}, -_MBID {MBID ? MBID->getAsString() : ""} -{ -} - -Release::pointer -Release::create(Session& session, const std::string& name, const std::optional& MBID) -{ - return session.getDboSession().add(std::unique_ptr {new Release {name, MBID}}); -} - -std::vector -Release::find(Session& session, const std::string& name) -{ - session.checkUniqueLocked(); - - auto res {session.getDboSession() - .find() - .where("name = ?").bind( std::string(name, 0, _maxNameLength) ) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -Release::pointer -Release::find(Session& session, const UUID& mbid) -{ - session.checkSharedLocked(); - - return session.getDboSession() - .find() - .where("mbid = ?").bind(std::string {mbid.getAsString()}) - .resultValue();; -} - -Release::pointer -Release::find(Session& session, ReleaseId id) -{ - session.checkSharedLocked(); - - return session.getDboSession() - .find() - .where("id = ?").bind(id) - .resultValue(); -} - -bool -Release::exists(Session& session, ReleaseId id) -{ - session.checkSharedLocked(); - return session.getDboSession().query("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; -} - -std::size_t -Release::getCount(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().query("SELECT COUNT(*) FROM release"); -} - -RangeResults -Release::findOrderedByArtist(Session& session, Range range) -{ - session.checkSharedLocked(); - - // TODO merge with find - auto query {session.getDboSession().query( - "SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON r.id = t.release_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" - " INNER JOIN artist a ON t_a_l.artist_id = a.id") - .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")}; - - return Utils::execQuery(query, range); -} - -RangeResults -Release::findOrphans(Session& session, Range range) -{ - session.checkSharedLocked(); - - auto query {session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")}; - return Utils::execQuery(query, range); -} - -RangeResults -Release::find(Session& session, const FindParameters& params) -{ - session.checkSharedLocked(); - - auto query {createQuery(session, params)}; - - return Utils::execQuery(query, params.range); -} - -std::size_t -Release::getDiscCount() const -{ - assert(session()); - int res {session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") - .join("release r ON r.id = t.release_id") - .where("r.id = ?") - .bind(getId())}; - - return res; -} - -Wt::WDate -Release::getReleaseDate() const -{ - return getReleaseDate(false); -} - -Wt::WDate -Release::getOriginalReleaseDate() const -{ - return getReleaseDate(true); -} - -Wt::WDate -Release::getReleaseDate(bool original) const -{ - assert(session()); - - const char* field {original ? "original_date" : "date"}; - - auto dates {session()->query( - std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") - .groupBy(field) - .bind(getId()) - .resultList()}; - - // various dates => invalid date - if (dates.empty() || dates.size() > 1) - return {}; - - return dates.front(); -} - -std::optional -Release::getCopyright() const -{ - assert(session()); - - Wt::Dbo::collection copyrights = session()->query - ("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") - .groupBy("copyright") - .bind(getId()); - - std::vector values(copyrights.begin(), copyrights.end()); - - // various copyrights => no copyright - if (values.empty() || values.size() > 1 || values.front().empty()) - return std::nullopt; - - return values.front(); -} - -std::optional -Release::getCopyrightURL() const -{ - assert(session()); - - Wt::Dbo::collection copyrights = session()->query - ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?") - .groupBy("copyright_url") - .bind(getId()); - - std::vector values(copyrights.begin(), copyrights.end()); - - // various copyright URLs => no copyright URL - if (values.empty() || values.size() > 1 || values.front().empty()) - return std::nullopt; - - return values.front(); -} - -std::vector -Release::getArtists(TrackArtistLinkType linkType) const -{ - assert(session()); - - auto res {session()->query>( - "SELECT DISTINCT a FROM artist a" - " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN release r ON r.id = t.release_id") - .where("r.id = ?").bind(getId()) - .where("t_a_l.type = ?").bind(linkType) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getSimilarReleases(std::optional offset, std::optional count) const -{ - assert(session()); - - auto res {session()->query>( - "SELECT r FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " WHERE " - " t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)" - " AND r.id <> ?" - ) - .bind(getId()) - .bind(getId()) - .groupBy("r.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(count ? static_cast(*count) : -1) - .offset(offset ? static_cast(*offset) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -bool -Release::hasVariousArtists() const -{ - // TODO optimize - return getArtists().size() > 1; -} - -std::size_t -Release::getTracksCount() const -{ - return _tracks.size(); -} - -std::chrono::milliseconds -Release::getDuration() const -{ - assert(session()); - - using milli = std::chrono::duration; - - Wt::Dbo::Query query {session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId())}; - - return query.resultValue(); -} - -Wt::WDateTime -Release::getLastWritten() const -{ - assert(session()); - - Wt::Dbo::Query query {session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") - .where("r.id = ?").bind(getId())}; - - return query.resultValue(); -} - -std::vector> -Release::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const -{ - assert(session()); - - WhereClause where; - - std::ostringstream oss; - - oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id "; - - where.And(WhereClause("r.id = ?")).bind(getId().toString()); - { - WhereClause clusterClause; - for (auto clusterType : clusterTypes) - clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); - where.And(clusterClause); - } - oss << " " << where.get(); - oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC"; - - auto query {session()->query>(oss.str())}; - for (const std::string& bindArg : where.getBindArgs()) - query.bind(bindArg); - - auto queryRes {query.resultList()}; - - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } - - std::vector> res; - for (const auto& [clusterTypeId, clusters] : clustersByType) - res.push_back(clusters); - - return res; -} + Wt::Dbo::Query createQuery(Session& session, const Release::FindParameters& params) + { + auto query{ session.getDboSession().query("SELECT DISTINCT r.id from release r") }; + + if (params.sortMethod == ReleaseSortMethod::LastWritten + || params.sortMethod == ReleaseSortMethod::Date + || params.sortMethod == ReleaseSortMethod::OriginalDate + || params.sortMethod == ReleaseSortMethod::OriginalDateDesc + || params.writtenAfter.isValid() + || params.dateRange + || params.artist.isValid()) + { + query.join("track t ON t.release_id = r.id"); + } + + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.dateRange) + { + query.where("t.date >= ?").bind(params.dateRange->begin); + query.where("t.date <= ?").bind(params.dateRange->end); + } + + for (std::string_view keyword : params.keywords) + query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%"); + + if (params.starringUser.isValid()) + { + assert(params.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) + .where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); + } + + if (params.artist.isValid()) + { + query.join("artist a ON a.id = t_a_l.artist_id") + .join("track_artist_link t_a_l ON t_a_l.track_id = t.id") + .where("a.id = ?").bind(params.artist); + + if (!params.trackArtistLinkTypes.empty()) + { + std::ostringstream oss; + + bool first{ true }; + for (TrackArtistLinkType linkType : params.trackArtistLinkTypes) + { + if (!first) + oss << " OR "; + oss << "t_a_l.type = ?"; + query.bind(linkType); + + first = false; + } + query.where(oss.str()); + } + + if (!params.excludedTrackArtistLinkTypes.empty()) + { + std::ostringstream oss; + oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" + " INNER JOIN artist a ON a.id = t_a_l.artist_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" + " INNER JOIN track t ON t.release_id = r.id" + " WHERE (a.id = ? AND ("; + + query.bind(params.artist); + + bool first{ true }; + for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes) + { + if (!first) + oss << " OR "; + oss << "t_a_l.type = ?"; + query.bind(linkType); + + first = false; + } + oss << ")))"; + query.where(oss.str()); + } + } + + if (!params.clusters.empty()) + { + std::ostringstream oss; + oss << "r.id IN (SELECT DISTINCT r.id FROM release r" + " INNER JOIN track t ON t.release_id = r.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + + WhereClause clusterClause; + for (const ClusterId clusterId : params.clusters) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(clusterId); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; + + query.where(oss.str()); + } + + if (params.primaryType) + query.where("primary_type = ?").bind(*params.primaryType); + if (!params.secondaryTypes.empty()) + query.where("secondary_type = ?").bind(params.secondaryTypes); + + switch (params.sortMethod) + { + case ReleaseSortMethod::None: + break; + case ReleaseSortMethod::Name: + query.orderBy("r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ReleaseSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case ReleaseSortMethod::Date: + query.orderBy("t.date, r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::OriginalDate: + query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::OriginalDateDesc: + query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_r.date_time DESC"); + break; + } + + return query; + } + + Release::Release(const std::string& name, const std::optional& MBID) + : _name{ std::string(name, 0 , _maxNameLength) }, + _MBID{ MBID ? MBID->getAsString() : "" } + { + } + + Release::pointer Release::create(Session& session, const std::string& name, const std::optional& MBID) + { + return session.getDboSession().add(std::unique_ptr {new Release{ name, MBID }}); + } + + std::vector Release::find(Session& session, const std::string& name) + { + session.checkUniqueLocked(); + + auto res{ session.getDboSession() + .find() + .where("name = ?").bind(std::string(name, 0, _maxNameLength)) + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + Release::pointer Release::find(Session& session, const UUID& mbid) + { + session.checkSharedLocked(); + + return session.getDboSession() + .find() + .where("mbid = ?").bind(std::string{ mbid.getAsString() }) + .resultValue();; + } + + Release::pointer Release::find(Session& session, ReleaseId id) + { + session.checkSharedLocked(); + + return session.getDboSession() + .find() + .where("id = ?").bind(id) + .resultValue(); + } + + bool Release::exists(Session& session, ReleaseId id) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; + } + + std::size_t Release::getCount(Session& session) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM release"); + } + + RangeResults Release::findOrderedByArtist(Session& session, Range range) + { + session.checkSharedLocked(); + + // TODO merge with find + auto query{ session.getDboSession().query( + "SELECT DISTINCT r.id FROM release r" + " INNER JOIN track t ON r.id = t.release_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" + " INNER JOIN artist a ON t_a_l.artist_id = a.id") + .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") }; + + return Utils::execQuery(query, range); + } + + RangeResults Release::findOrphans(Session& session, Range range) + { + session.checkSharedLocked(); + + auto query{ session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; + return Utils::execQuery(query, range); + } + + RangeResults Release::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); + + auto query{ createQuery(session, params) }; + + return Utils::execQuery(query, params.range); + } + + std::size_t Release::getDiscCount() const + { + assert(session()); + int res{ session()->query("SELECT COUNT(DISTINCT disc_number) FROM track t") + .join("release r ON r.id = t.release_id") + .where("r.id = ?") + .bind(getId()) }; + + return res; + } + + std::vector Release::getDiscs() const + { + assert(session()); + using ResultType = std::tuple; + auto results{ session()->query("SELECT DISTINCT disc_number, disc_subtitle FROM track t") + .join("release r ON r.id = t.release_id") + .where("r.id = ?") + .orderBy("disc_number") + .bind(getId()) + .resultList() }; + + std::vector discs; + for (const auto& res : results) + discs.emplace_back(DiscInfo{ static_cast(std::get(res)), std::get(res) }); + + return discs; + } + + Wt::WDate Release::getReleaseDate() const + { + return getReleaseDate(false); + } + + Wt::WDate Release::getOriginalReleaseDate() const + { + return getReleaseDate(true); + } + + Wt::WDate Release::getReleaseDate(bool original) const + { + assert(session()); + + const char* field{ original ? "original_date" : "date" }; + + auto dates{ session()->query( + std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?") + .groupBy(field) + .bind(getId()) + .resultList() }; + + // various dates => invalid date + if (dates.empty() || dates.size() > 1) + return {}; + + return dates.front(); + } + + std::optional Release::getCopyright() const + { + assert(session()); + + Wt::Dbo::collection copyrights = session()->query + ("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?") + .groupBy("copyright") + .bind(getId()); + + std::vector values(copyrights.begin(), copyrights.end()); + + // various copyrights => no copyright + if (values.empty() || values.size() > 1 || values.front().empty()) + return std::nullopt; + + return values.front(); + } + + std::optional Release::getCopyrightURL() const + { + assert(session()); + + Wt::Dbo::collection copyrights = session()->query + ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?") + .groupBy("copyright_url") + .bind(getId()); + + std::vector values(copyrights.begin(), copyrights.end()); + + // various copyright URLs => no copyright URL + if (values.empty() || values.size() > 1 || values.front().empty()) + return std::nullopt; + + return values.front(); + } + + std::vector Release::getArtists(TrackArtistLinkType linkType) const + { + assert(session()); + + auto res{ session()->query>( + "SELECT DISTINCT a FROM artist a" + " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" + " INNER JOIN track t ON t.id = t_a_l.track_id" + " INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?").bind(getId()) + .where("t_a_l.type = ?").bind(linkType) + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + std::vector Release::getSimilarReleases(std::optional offset, std::optional count) const + { + assert(session()); + + auto res{ session()->query>( + "SELECT r FROM release r" + " INNER JOIN track t ON t.release_id = r.id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " WHERE " + " t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)" + " AND r.id <> ?" + ) + .bind(getId()) + .bind(getId()) + .groupBy("r.id") + .orderBy("COUNT(*) DESC, RANDOM()") + .limit(count ? static_cast(*count) : -1) + .offset(offset ? static_cast(*offset) : -1) + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + bool Release::hasVariousArtists() const + { + // TODO optimize + return getArtists().size() > 1; + } + + std::size_t Release::getTracksCount() const + { + return _tracks.size(); + } + + std::chrono::milliseconds Release::getDuration() const + { + assert(session()); + + using milli = std::chrono::duration; + + Wt::Dbo::Query query{ session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") + .where("r.id = ?").bind(getId()) }; + + return query.resultValue(); + } + + Wt::WDateTime Release::getLastWritten() const + { + assert(session()); + + Wt::Dbo::Query query{ session()->query("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") + .where("r.id = ?").bind(getId()) }; + + return query.resultValue(); + } + + std::vector> Release::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const + { + assert(session()); + + WhereClause where; + + std::ostringstream oss; + + oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id "; + + where.And(WhereClause("r.id = ?")).bind(getId().toString()); + { + WhereClause clusterClause; + for (auto clusterType : clusterTypes) + clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); + where.And(clusterClause); + } + oss << " " << where.get(); + oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC"; + + auto query{ session()->query>(oss.str()) }; + for (const std::string& bindArg : where.getBindArgs()) + query.bind(bindArg); + + auto queryRes{ query.resultList() }; + + std::map> clustersByType; + for (const Wt::Dbo::ptr& cluster : queryRes) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + } + + std::vector> res; + for (const auto& [clusterTypeId, clusters] : clustersByType) + res.push_back(clusters); + + return res; + } } // namespace Database diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 27276fc7..810a4067 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -37,127 +37,127 @@ namespace Database { -class Artist; -class Cluster; -class ClusterType; -class Release; -class Session; -class Track; -class User; + class Artist; + class Cluster; + class ClusterType; + class Release; + class Session; + class Track; + class User; -class Release final : public Object -{ - public: - struct FindParameters - { - std::vector clusters; // if non empty, releases that belong to these clusters - std::vector keywords; // if non empty, name must match all of these keywords - ReleaseSortMethod sortMethod {ReleaseSortMethod::None}; - Range range; - Wt::WDateTime writtenAfter; - std::optional dateRange; - UserId starringUser; // only releases starred by this user - std::optional scrobbler; // and for this scrobbler - ArtistId artist; // only releases that involved this user - EnumSet trackArtistLinkTypes; // and for these link types - EnumSet excludedTrackArtistLinkTypes; // but not for these link types - std::optional primaryType; // if, set, matching this primary type - EnumSet secondaryTypes; // Matching all this (if any) + class Release final : public Object + { + public: + struct FindParameters + { + std::vector clusters; // if non empty, releases that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; + Range range; + Wt::WDateTime writtenAfter; + std::optional dateRange; + UserId starringUser; // only releases starred by this user + std::optional scrobbler; // and for this scrobbler + ArtistId artist; // only releases that involved this user + EnumSet trackArtistLinkTypes; // and for these link types + EnumSet excludedTrackArtistLinkTypes; // but not for these link types + std::optional primaryType; // if, set, matching this primary type + EnumSet secondaryTypes; // Matching all this (if any) - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } - FindParameters& setKeywords(const std::vector& _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; return *this; } - FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } - FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}, EnumSet _excludedTrackArtistLinkTypes = {}) - { - artist = _artist; - trackArtistLinkTypes = _trackArtistLinkTypes; - excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes; - return *this; - } - }; + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _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; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}, EnumSet _excludedTrackArtistLinkTypes = {}) + { + artist = _artist; + trackArtistLinkTypes = _trackArtistLinkTypes; + excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes; + return *this; + } + }; - Release() = default; + Release() = default; - // Accessors - static std::size_t getCount(Session& session); - static bool exists(Session& session, ReleaseId id); - static pointer find(Session& session, const UUID& MBID); - static std::vector find(Session& session, const std::string& name); - static pointer find(Session& session, ReleaseId id); - static RangeResults find(Session& session, const FindParameters& parameters); - static RangeResults findOrphans(Session& session, Range range); // no track related - static RangeResults findOrderedByArtist(Session& session, Range range); + // Accessors + static std::size_t getCount(Session& session); + static bool exists(Session& session, ReleaseId id); + static pointer find(Session& session, const UUID& MBID); + static std::vector find(Session& session, const std::string& name); + static pointer find(Session& session, ReleaseId id); + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findOrphans(Session& session, Range range); // no track related + static RangeResults findOrderedByArtist(Session& session, Range range); - std::size_t getTracksCount() const; + // Get the cluster of the tracks that belong to this release + // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) + // size is the max number of cluster per cluster type + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - // Get the cluster of the tracks that belong to this release - // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) - // size is the max number of cluster per cluster type - std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; + // Utility functions (if all tracks have the same values, which is legit to not be the case) + Wt::WDate getReleaseDate() const; + Wt::WDate getOriginalReleaseDate() const; + std::optional getCopyright() const; + std::optional getCopyrightURL() const; - // Utility functions (if all tracks have the same values, which is legit to not be the case) - Wt::WDate getReleaseDate() const; - Wt::WDate getOriginalReleaseDate() const; - std::optional getCopyright() const; - std::optional getCopyrightURL() const; + // Accessors + const std::string& getName() const { return _name; } + std::optional getMBID() const { return UUID::fromString(_MBID); } + std::optional getTotalDisc() const { return _totalDisc; } + std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) + std::vector getDiscs() const; + std::chrono::milliseconds getDuration() const; + Wt::WDateTime getLastWritten() const; + std::optional getPrimaryType() const { return _primaryType; } + EnumSet getSecondaryTypes() const { return _secondaryTypes; } + std::size_t getTracksCount() const; - // Accessors - const std::string& getName() const { return _name; } - std::optional getMBID() const { return UUID::fromString(_MBID); } - std::optional getTotalDisc() const { return _totalDisc; } - std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) - std::chrono::milliseconds getDuration() const; - Wt::WDateTime getLastWritten() const; - std::optional getPrimaryType() const { return _primaryType; } - EnumSet getSecondaryTypes() const { return _secondaryTypes; } + // Setters + void setName(std::string_view name) { _name = name; } + void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } + void setTotalDisc(std::optional totalDisc) { _totalDisc = totalDisc; } + void setPrimaryType(std::optional type) { _primaryType = type; } + void setSecondaryTypes(EnumSet types) { _secondaryTypes = types; } - // Setters - void setName(std::string_view name) { _name = name; } - void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } - void setTotalDisc(std::optional totalDisc) { _totalDisc = totalDisc; } - void setPrimaryType(std::optional type) { _primaryType = type; } - void setSecondaryTypes(EnumSet types) { _secondaryTypes = types; } - - // Get the artists of this release - std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; - std::vector> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } - bool hasVariousArtists() const; - std::vector getSimilarReleases(std::optional offset = {}, std::optional count = {}) const; + // Get the artists of this release + std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; + std::vector> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } + bool hasVariousArtists() const; + std::vector getSimilarReleases(std::optional offset = {}, std::optional count = {}) const; - template - void persist(Action& a) - { - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _MBID, "mbid"); - Wt::Dbo::field(a, _totalDisc, "total_disc"); - Wt::Dbo::field(a, _primaryType, "primary_type"); - Wt::Dbo::field(a, _secondaryTypes, "secondary_types"); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _MBID, "mbid"); + Wt::Dbo::field(a, _totalDisc, "total_disc"); + Wt::Dbo::field(a, _primaryType, "primary_type"); + Wt::Dbo::field(a, _secondaryTypes, "secondary_types"); - Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); - } + Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); + } - private: - friend class Session; - Release(const std::string& name, const std::optional& MBID = {}); - static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); + private: + friend class Session; + Release(const std::string& name, const std::optional& MBID = {}); + static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - Wt::WDate getReleaseDate(bool original) const; + Wt::WDate getReleaseDate(bool original) const; - static constexpr std::size_t _maxNameLength {128}; + static constexpr std::size_t _maxNameLength{ 128 }; - std::string _name; - std::string _MBID; - std::optional _totalDisc {}; - std::optional _primaryType; - EnumSet _secondaryTypes; + std::string _name; + std::string _MBID; + std::optional _totalDisc{}; + std::optional _primaryType; + EnumSet _secondaryTypes; - Wt::Dbo::collection> _tracks; // Tracks in the release -}; + Wt::Dbo::collection> _tracks; // Tracks in the release + }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index 0e50cb39..3266c582 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -83,6 +83,12 @@ namespace Database static DateRange fromYearRange(int from, int to); }; + struct DiscInfo + { + std::size_t position; + std::string name; + }; + enum class ArtistSortMethod { None, diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 118548a7..cd6f55a8 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(lmssubsonic SHARED impl/responses/Artist.cpp impl/responses/Bookmark.cpp impl/responses/Contributor.cpp + impl/responses/DiscTitle.cpp impl/responses/Genre.cpp impl/responses/Playlist.cpp impl/responses/Song.cpp diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 2998a70d..3b80a29b 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -28,6 +28,7 @@ #include "utils/String.hpp" #include "responses/Artist.hpp" +#include "responses/DiscTitle.hpp" #include "SubsonicId.hpp" namespace API::Subsonic @@ -175,6 +176,11 @@ namespace API::Subsonic for (const ReleaseTypeSecondary releaseType : release->getSecondaryTypes()) albumNode.addArrayValue("releaseTypes", toString(releaseType)); + // disc titles + albumNode.createEmptyArrayChild("discTitles"); + for (const DiscInfo& discInfo : release->getDiscs()) + albumNode.addArrayChild("discTitles", createDiscTitle(discInfo)); + return albumNode; } } \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/DiscTitle.cpp b/src/libs/subsonic/impl/responses/DiscTitle.cpp new file mode 100644 index 00000000..53ec3215 --- /dev/null +++ b/src/libs/subsonic/impl/responses/DiscTitle.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "responses/DiscTitle.hpp" + +namespace API::Subsonic +{ + Response::Node createDiscTitle(const Database::DiscInfo& discInfo) + { + Response::Node discTitleNode; + + discTitleNode.setAttribute("disc", discInfo.position); + discTitleNode.setAttribute("title", discInfo.name); + + return discTitleNode; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/DiscTitle.hpp b/src/libs/subsonic/impl/responses/DiscTitle.hpp new file mode 100644 index 00000000..f9ff1185 --- /dev/null +++ b/src/libs/subsonic/impl/responses/DiscTitle.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/Types.hpp" +#include "SubsonicResponse.hpp" + +namespace API::Subsonic +{ + Response::Node createDiscTitle(const Database::DiscInfo& discInfo); +} \ No newline at end of file