diff --git a/approot/messages.xml b/approot/messages.xml index 8b4557e9..9d3ad6f4 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -33,6 +33,7 @@ Error occured! Go home Release not found +Playlist not found You are not allowed to perform this operation User not found @@ -125,6 +126,7 @@ Play shuffled Random Recently added +Recently modified Recently played Albums Star @@ -155,6 +157,12 @@ Copyright Disc {1} + + + {1} track + {1} tracks + + Delete playlist? diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index e3423213..9da6e08a 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -33,6 +33,7 @@ Une erreur est survenue! Retour à l'accueil Cet album n'existe pas +Cette liste de lecture n'existe pas Vous n'avez pas les droits pour effectuer cette opération L'utilisateur n'existe pas @@ -125,6 +126,7 @@ Jouer aléatoirement Aléatoire Ajoutés récemment +Modifiés récemment Joués récemment Albums Ajouter aux favoris @@ -155,6 +157,12 @@ Copyright Disque {1} + + + {1} piste + {1} pistes + + Supprimer la liste de lecture ? diff --git a/approot/messages_it.xml b/approot/messages_it.xml index a7574885..e779790f 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -33,6 +33,7 @@ C'è stato un errore! Ritorna alla home Album non trovato + Non sei autorizzato ad eseguire questa operazione Utente non trovato @@ -124,6 +125,7 @@ Aggiungi alla coda di riproduzione mischiando Casuali Aggiunti di recente + Riprodotti di recente Album Aggiungi ai preferiti @@ -154,8 +156,15 @@ Copyright Disco {1} + + + + + + + Ricerca... diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 9246e707..ac634b2d 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -33,6 +33,7 @@ 发生错误! 返回主页 发布未找到 + 不允许执行此操作 未找到此用户 @@ -125,6 +126,7 @@ 随机播放 随机 最近添加 + 最近播放 专辑 收藏 @@ -155,7 +157,13 @@ 版权所有 唱片 {1} - + + + + + + + 搜索中... diff --git a/approot/tracklist.xml b/approot/tracklist.xml new file mode 100644 index 00000000..093a7884 --- /dev/null +++ b/approot/tracklist.xml @@ -0,0 +1,39 @@ + + + + + +
+

${name}

+
${track-count} · ${duration}
+
+ ${play-btn class="btn btn-primary"} +
+
+ ${tracks} +
+ + + + + +
diff --git a/approot/tracklists.xml b/approot/tracklists.xml index 86560654..7bb0549a 100644 --- a/approot/tracklists.xml +++ b/approot/tracklists.xml @@ -3,51 +3,26 @@ +
+ +
${tracklists}
-
-
-
${name}
-
-
- ${duration} -
-
- ${play-btn class="btn btn-sm btn-outline-secondary border-0"} - -
-
+ ${name class="text-decoration-none link-secondary"}
- ${elements} + ${elements class="d-grid gap-3"} ${loading-indicator} - - - -
diff --git a/src/libs/services/auth/impl/AuthServiceBase.cpp b/src/libs/services/auth/impl/AuthServiceBase.cpp index 01b87a0a..133ce8f5 100644 --- a/src/libs/services/auth/impl/AuthServiceBase.cpp +++ b/src/libs/services/auth/impl/AuthServiceBase.cpp @@ -45,7 +45,7 @@ namespace Auth LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN); - user = User::create(session, loginName); + user = session.create(loginName); user.modify()->setType(type); } diff --git a/src/libs/services/auth/impl/AuthTokenService.cpp b/src/libs/services/auth/impl/AuthTokenService.cpp index 01ac9c87..7decbc14 100644 --- a/src/libs/services/auth/impl/AuthTokenService.cpp +++ b/src/libs/services/auth/impl/AuthTokenService.cpp @@ -60,7 +60,7 @@ namespace Auth if (!user) throw Exception {"User deleted"}; - Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)}; + Database::AuthToken::pointer authToken {session.create(secretHash, expiry, user)}; LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString(); diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index 7d904bd8..c267b3c9 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -40,6 +40,12 @@ _MBID {MBID ? MBID->getAsString() : ""} { } +Artist::pointer +Artist::create(Session& session, const std::string& name, const std::optional& MBID) +{ + return session.getDboSession().add(std::unique_ptr {new Artist {name, MBID}}); +} + std::size_t Artist::getCount(Session& session) { @@ -81,17 +87,6 @@ Artist::exists(Session& session, ArtistId id) return session.getDboSession().query("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1; } -Artist::pointer -Artist::create(Session& session, const std::string& name, const std::optional& MBID) -{ - session.checkUniqueLocked(); - - Artist::pointer res {session.getDboSession().add(std::make_unique(name, MBID))}; - session.getDboSession().flush(); - - return res; -} - static Wt::Dbo::Query createQuery(Session& session, const Artist::FindParameters& params) @@ -119,13 +114,13 @@ createQuery(Session& session, const Artist::FindParameters& params) for (std::string_view keyword : params.keywords) { clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); - query.bind("%" + escapeLikeKeyword(keyword) + "%"); + query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%"); } for (std::string_view keyword : params.keywords) { sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); - query.bind("%" + escapeLikeKeyword(keyword) + "%"); + query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%"); } query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")"); @@ -193,7 +188,7 @@ Artist::findAllOrphans(Session& session, Range range) session.checkSharedLocked(); auto query {session.getDboSession().query("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); + return Utils::execQuery(query, range); } RangeResults @@ -202,7 +197,7 @@ Artist::find(Session& session, const FindParameters& params) session.checkSharedLocked(); auto query {createQuery(session, params)}; - return execQuery(query, params.range); + return Utils::execQuery(query, params.range); } RangeResults @@ -246,10 +241,11 @@ Artist::findSimilarArtists(EnumSet artistLinkTypes, Range r .bind(getId()) .groupBy("a.id") .orderBy("COUNT(*) DESC, RANDOM()")}; + for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - return execQuery(query, range); + return Utils::execQuery(query, range); } std::vector> diff --git a/src/libs/services/database/impl/AuthToken.cpp b/src/libs/services/database/impl/AuthToken.cpp index 85058a01..b2702d00 100644 --- a/src/libs/services/database/impl/AuthToken.cpp +++ b/src/libs/services/database/impl/AuthToken.cpp @@ -38,12 +38,7 @@ namespace Database AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) { - session.checkUniqueLocked(); - - AuthToken::pointer res {session.getDboSession().add(std::make_unique(value, expiry, user))}; - session.getDboSession().flush(); - - return res; + return session.getDboSession().add(std::unique_ptr {new AuthToken {value, expiry, user}}); } void diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp index 87ec28b1..ed01031a 100644 --- a/src/libs/services/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -36,6 +36,12 @@ Cluster::Cluster(ObjectPtr type, std::string_view name) { } +Cluster::pointer +Cluster::create(Session& session, ObjectPtr type, std::string_view name) +{ + return session.getDboSession().add(std::unique_ptr {new Cluster {type, name}}); +} + std::size_t Cluster::getCount(Session& session) { @@ -44,24 +50,13 @@ Cluster::getCount(Session& session) return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); } -Cluster::pointer -Cluster::create(Session& session, ObjectPtr type, std::string_view name) -{ - session.checkUniqueLocked(); - - Cluster::pointer res {session.getDboSession().add(std::make_unique(type, name))}; - session.getDboSession().flush(); - - return res; -} - RangeResults Cluster::find(Session& session, Range range) { session.checkSharedLocked(); auto query {session.getDboSession().query("SELECT id FROM cluster")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -70,7 +65,7 @@ Cluster::findOrphans(Session& session, Range range) session.checkSharedLocked(); auto query {session.getDboSession().query("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 execQuery(query, range); + return Utils::execQuery(query, range); } Cluster::pointer @@ -95,7 +90,7 @@ Cluster::getTracks(Range range) const auto query {session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("c.id = ?").bind(getId())}; - return execQuery(query, range); + return Utils::execQuery(query, range); } std::size_t @@ -113,6 +108,12 @@ ClusterType::ClusterType(std::string_view name) { } +ClusterType::pointer +ClusterType::create(Session& session, const std::string& name) +{ + return session.getDboSession().add(std::unique_ptr {new ClusterType {name}}); +} + std::size_t ClusterType::getCount(Session& session) { @@ -132,7 +133,7 @@ ClusterType::findOrphans(Session& session, Range range) " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") .where("c.id IS NULL")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -144,7 +145,7 @@ ClusterType::findUsed(Session& session, Range range) "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); + return Utils::execQuery(query, range); } ClusterType::pointer @@ -170,18 +171,7 @@ ClusterType::find(Session& session, Range range) auto query {session.getDboSession().query("SELECT id from cluster_type")}; - return execQuery(query, range); -} - -ClusterType::pointer -ClusterType::create(Session& session, const std::string& name) -{ - session.checkUniqueLocked(); - - ClusterType::pointer res {session.getDboSession().add(std::make_unique(name))}; - session.getDboSession().flush(); - - return res; + return Utils::execQuery(query, range); } Cluster::pointer diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp index c9a2cb45..8e37711c 100644 --- a/src/libs/services/database/impl/Listen.cpp +++ b/src/libs/services/database/impl/Listen.cpp @@ -141,6 +141,12 @@ namespace Database , _track {getDboPtr(track)} {} + Listen::pointer + Listen::create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + { + return session.getDboSession().add(std::unique_ptr {new Listen {user, track, scrobbler, dateTime}}); + } + std::size_t Listen::getCount(Session& session) { @@ -172,7 +178,7 @@ namespace Database if (parameters.scrobblingState) query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); - return execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } Listen::pointer @@ -188,17 +194,6 @@ namespace Database .resultValue(); } - Listen::pointer - Listen::create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) - { - session.checkUniqueLocked(); - - Listen::pointer res {session.getDboSession().add(std::make_unique(user, track, scrobbler, dateTime))}; - session.getDboSession().flush(); - - return res; - } - RangeResults Listen::getTopArtists(Session& session, UserId userId, @@ -213,7 +208,7 @@ namespace Database .orderBy("COUNT(a.id) DESC") .groupBy("a.id")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -227,7 +222,7 @@ namespace Database .orderBy("COUNT(r.id) DESC") .groupBy("r.id")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -241,7 +236,7 @@ namespace Database .orderBy("COUNT(t.id) DESC") .groupBy("t.id")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -256,7 +251,7 @@ namespace Database .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -270,7 +265,7 @@ namespace Database .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -284,7 +279,7 @@ namespace Database .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index a2c9616c..ef897499 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -576,12 +576,21 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( migrateFromV34(Session& session) { // Add scrobbling state - // By default, everythin needs to be sent + // By default, everything needs to be sent session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*ScrobblingState::PendingAdd*/0)) + ")"); } + static + void + migrateFromV35(Session& session) + { + // Add creattion/last modif date time for tracklists + session.getDboSession().execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); + session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); + } + void doDbMigration(Session& session) { @@ -623,6 +632,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( {32, migrateFromV32}, {33, migrateFromV33}, {34, migrateFromV34}, + {35, migrateFromV35}, }; while (1) diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 65b8b284..2e7f2f04 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {35}; + static constexpr Version LMS_DATABASE_VERSION {36}; class VersionInfo { public: diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 3ca55456..1b2bf630 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -57,7 +57,7 @@ createQuery(Session& session, const Release::FindParameters& params) } 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("%" + Utils::escapeLikeKeyword(keyword) + "%"); if (params.starringUser.isValid()) { @@ -172,6 +172,12 @@ _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) { @@ -214,17 +220,6 @@ Release::exists(Session& session, ReleaseId id) return session.getDboSession().query("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; } -Release::pointer -Release::create(Session& session, const std::string& name, const std::optional& MBID) -{ - session.checkSharedLocked(); - - Release::pointer res {session.getDboSession().add(std::make_unique(name, MBID))}; - session.getDboSession().flush(); - - return res; -} - std::size_t Release::getCount(Session& session) { @@ -238,7 +233,7 @@ Release::findOrderedByArtist(Session& session, Range range) { session.checkSharedLocked(); - // TODO merge with execQuery + // 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" @@ -246,7 +241,7 @@ Release::findOrderedByArtist(Session& session, Range range) " INNER JOIN artist a ON t_a_l.artist_id = a.id") .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -255,7 +250,7 @@ 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 execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -265,7 +260,7 @@ Release::find(Session& session, const FindParameters& params) auto query {createQuery(session, params)}; - return execQuery(query, params.range); + return Utils::execQuery(query, params.range); } std::optional diff --git a/src/libs/services/database/impl/ScanSettings.cpp b/src/libs/services/database/impl/ScanSettings.cpp index 13005720..45ff9f38 100644 --- a/src/libs/services/database/impl/ScanSettings.cpp +++ b/src/libs/services/database/impl/ScanSettings.cpp @@ -120,7 +120,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set& clu if (!clusterType) { LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName; - clusterType = ClusterType::create(session, clusterTypeName); + clusterType = session.create(clusterTypeName); _clusterTypes.insert(getDboPtr(clusterType)); needRescan = true; diff --git a/src/libs/services/database/impl/StarredArtist.cpp b/src/libs/services/database/impl/StarredArtist.cpp index 1960a66e..ca4b3638 100644 --- a/src/libs/services/database/impl/StarredArtist.cpp +++ b/src/libs/services/database/impl/StarredArtist.cpp @@ -36,6 +36,12 @@ namespace Database { } + StarredArtist::pointer + StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) + { + return session.getDboSession().add(std::unique_ptr {new StarredArtist {artist, user, scrobbler}}); + } + std::size_t StarredArtist::getCount(Session& session) { @@ -61,20 +67,9 @@ namespace Database .resultValue(); } - StarredArtist::pointer - StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) - { - session.checkUniqueLocked(); - - StarredArtist::pointer res {session.getDboSession().add(std::make_unique(artist, user, scrobbler))}; - session.getDboSession().flush(); - - return res; - } - void StarredArtist::setDateTime(const Wt::WDateTime& dateTime) { - _dateTime = normalizeDateTime(dateTime); + _dateTime = Utils::normalizeDateTime(dateTime); } } diff --git a/src/libs/services/database/impl/StarredRelease.cpp b/src/libs/services/database/impl/StarredRelease.cpp index 22b556bf..17f1c598 100644 --- a/src/libs/services/database/impl/StarredRelease.cpp +++ b/src/libs/services/database/impl/StarredRelease.cpp @@ -36,6 +36,12 @@ namespace Database { } + StarredRelease::pointer + StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) + { + return session.getDboSession().add(std::unique_ptr{new StarredRelease{release, user, scrobbler}}); + } + std::size_t StarredRelease::getCount(Session& session) { @@ -61,20 +67,9 @@ namespace Database .resultValue(); } - StarredRelease::pointer - StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) - { - session.checkUniqueLocked(); - - StarredRelease::pointer res {session.getDboSession().add(std::make_unique(release, user, scrobbler))}; - session.getDboSession().flush(); - - return res; - } - void StarredRelease::setDateTime(const Wt::WDateTime& dateTime) { - _dateTime = normalizeDateTime(dateTime); + _dateTime = Utils::normalizeDateTime(dateTime); } } diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp index b2ddb486..21158c9a 100644 --- a/src/libs/services/database/impl/StarredTrack.cpp +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -36,6 +36,12 @@ namespace Database { } + StarredTrack::pointer + StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) + { + return session.getDboSession().add(std::unique_ptr {new StarredTrack{track, user, scrobbler}}); + } + std::size_t StarredTrack::getCount(Session& session) { @@ -75,23 +81,12 @@ namespace Database if (params.user.isValid()) query.where("s_t.user_id = ?").bind(params.user); - return execQuery(query, params.range); - } - - StarredTrack::pointer - StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) - { - session.checkUniqueLocked(); - - StarredTrack::pointer res {session.getDboSession().add(std::make_unique(track, user, scrobbler))}; - session.getDboSession().flush(); - - return res; + return Utils::execQuery(query, params.range); } void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) { - _dateTime = normalizeDateTime(dateTime); + _dateTime = Utils::normalizeDateTime(dateTime); } } diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index 66cc6d1a..b6f22f7d 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -46,7 +46,7 @@ createQuery(Session& session, const Track::FindParameters& params) auto query {session.getDboSession().query("SELECT DISTINCT t.id from track t")}; 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("%" + Utils::escapeLikeKeyword(keyword) + "%"); if (params.writtenAfter.isValid()) query.where("t.file_last_write > ?").bind(params.writtenAfter); @@ -156,12 +156,7 @@ 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(p))}; - session.getDboSession().flush(); - - return res; + return session.getDboSession().add(std::unique_ptr {new Track {p}}); } std::size_t @@ -219,7 +214,7 @@ Track::findPaths(Session& session, Range range) // TODO Dbo traits on filesystem auto query {session.getDboSession().query("SELECT id, file_path FROM track")}; - RangeResults queryResults {execQuery(query, range)}; + RangeResults queryResults {Utils::execQuery(query, range)}; RangeResults res; res.range = queryResults.range; @@ -243,7 +238,7 @@ Track::findRecordingMBIDDuplicates(Session& session, Range range) auto query {session.getDboSession().query( "SELECT track.id FROM track WHERE recording_mbid in (SELECT recording_mbid FROM track WHERE recording_mbid <> '' GROUP BY recording_mbid HAVING COUNT (*) > 1)") .orderBy("track.release_id,track.disc_number,track.track_number,track.recording_mbid")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults @@ -255,7 +250,7 @@ Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range) .where("LENGTH(t.recording_mbid) > 0") .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")}; - return execQuery(query, range); + return Utils::execQuery(query, range); } std::vector @@ -284,7 +279,7 @@ Track::find(Session& session, const FindParameters& parameters) auto query {createQuery(session, parameters)}; - return execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } RangeResults @@ -297,7 +292,7 @@ Track::findByNameAndReleaseName(Session& session, std::string_view trackName, st .where("t.name = ?").bind(trackName) .where("r.name = ?").bind(releaseName)}; - return execQuery(query, Range {}); + return Utils::execQuery(query, Range {}); } RangeResults @@ -328,7 +323,7 @@ Track::findSimilarTracks(Session& session, const std::vector& tracks, R for (TrackId trackId : tracks) query.bind(trackId); - return execQuery(query, range); + return Utils::execQuery(query, range); } void diff --git a/src/libs/services/database/impl/TrackBookmark.cpp b/src/libs/services/database/impl/TrackBookmark.cpp index 28e169ca..71b09708 100644 --- a/src/libs/services/database/impl/TrackBookmark.cpp +++ b/src/libs/services/database/impl/TrackBookmark.cpp @@ -33,6 +33,12 @@ _track {getDboPtr(track)} { } +TrackBookmark::pointer +TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) +{ + return session.getDboSession().add(std::unique_ptr {new TrackBookmark {user, track}}); +} + std::size_t TrackBookmark::getCount(Session& session) { @@ -41,18 +47,6 @@ TrackBookmark::getCount(Session& session) return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); } - -TrackBookmark::pointer -TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) -{ - session.checkUniqueLocked(); - - TrackBookmark::pointer res {session.getDboSession().add(std::make_unique(user, track))}; - session.getDboSession().flush(); - - return res; -} - RangeResults TrackBookmark::find(Session& session, UserId userId, Range range) { @@ -61,7 +55,7 @@ TrackBookmark::find(Session& session, UserId userId, Range range) auto query {session.getDboSession().query("SELECT id from track_bookmark") .where("user_id = ?").bind(userId)}; - return execQuery(query, range); + return Utils::execQuery(query, range); } TrackBookmark::pointer diff --git a/src/libs/services/database/impl/TrackFeatures.cpp b/src/libs/services/database/impl/TrackFeatures.cpp index 628c10c7..35f92b27 100644 --- a/src/libs/services/database/impl/TrackFeatures.cpp +++ b/src/libs/services/database/impl/TrackFeatures.cpp @@ -36,6 +36,12 @@ _track {getDboPtr(track)} { } +TrackFeatures::pointer +TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) +{ + return session.getDboSession().add(std::unique_ptr {new TrackFeatures {track, jsonEncodedFeatures}}); +} + std::size_t TrackFeatures::getCount(Session& session) { @@ -71,18 +77,7 @@ TrackFeatures::find(Session& session, Range range) auto query {session.getDboSession().query("SELECT id from track_features")}; - return execQuery(query, range); -} - -TrackFeatures::pointer -TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) -{ - session.checkUniqueLocked(); - - TrackFeatures::pointer res {session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures))}; - session.getDboSession().flush(); - - return res; + return Utils::execQuery(query, range); } FeatureValues diff --git a/src/libs/services/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp index 4f2bb0ba..504e09b9 100644 --- a/src/libs/services/database/impl/TrackList.cpp +++ b/src/libs/services/database/impl/TrackList.cpp @@ -36,23 +36,20 @@ namespace Database { TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) -: _name {name}, - _type {type}, - _isPublic {isPublic}, - _user {getDboPtr(user)} + : _name {name} +, _type {type} +, _isPublic {isPublic} +, _creationDateTime {Wt::WDateTime::currentDateTime()} +, _lastModifiedDateTime {Wt::WDateTime::currentDateTime()} +, _user {getDboPtr(user)} { + assert(user); } TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) { - session.checkUniqueLocked(); - assert(user); - - TrackList::pointer res {session.getDboSession().add( std::make_unique(name, type, isPublic, user) )}; - session.getDboSession().flush(); - - return res; + return session.getDboSession().add(std::unique_ptr {new TrackList {name, type, isPublic, user}}); } std::size_t @@ -112,9 +109,19 @@ TrackList::find(Session& session, const FindParameters& params) query.where(oss.str()); } - query.orderBy("t_l.name COLLATE NOCASE"); + switch (params.sortMethod) + { + case TrackListSortMethod::None: + break; + case TrackListSortMethod::Name: + query.orderBy("t_l.name COLLATE NOCASE"); + break; + case TrackListSortMethod::LastModifiedDesc: + query.orderBy("t_l.last_modified_date_time DESC"); + break; + } - return execQuery(query, params.range); + return Utils::execQuery(query, params.range); } TrackList::pointer @@ -173,7 +180,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTim return session()->find() .where("tracklist_id = ?").bind(getId()) .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(normalizeDateTime(dateTime)) + .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) .resultValue(); } @@ -596,23 +603,30 @@ TrackList::getTopTracks(const std::vector& clusterIds, std::optional< } TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -: _dateTime {normalizeDateTime(dateTime)} +: _dateTime {Utils::normalizeDateTime(dateTime)} , _track {getDboPtr(track)} , _tracklist {getDboPtr(tracklist)} { + assert(track); + assert(tracklist); } TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) { - session.checkUniqueLocked(); - assert(track); - assert(tracklist); + return session.getDboSession().add(std::unique_ptr( new TrackListEntry {track, tracklist, dateTime})); +} - auto res = session.getDboSession().add(std::make_unique( track, tracklist, dateTime)); - session.getDboSession().flush(); +void +TrackListEntry::onPostCreated() +{ + _tracklist.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); +} - return res; +void +TrackListEntry::onPreRemove() +{ + _tracklist.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); } TrackListEntry::pointer diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index 31728e06..f7a4d4c0 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -35,6 +35,12 @@ User::User(std::string_view loginName) { } +User::pointer +User::create(Session& session, std::string_view loginName) +{ + return session.getDboSession().add(std::unique_ptr {new User {loginName}}); +} + std::size_t User::getCount(Session& session) { @@ -53,7 +59,7 @@ User::find(Session& session, const FindParameters& params) if (params.scrobbler) query.where("scrobbler = ?").bind(*params.scrobbler); - return execQuery(query, params.range); + return Utils::execQuery(query, params.range); } User::pointer @@ -64,17 +70,6 @@ User::findDemoUser(Session& session) return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); } -User::pointer -User::create(Session& session, std::string_view loginName) -{ - session.checkUniqueLocked(); - - User::pointer user {session.getDboSession().add(std::make_unique(loginName))}; - session.getDboSession().flush(); - - return user; -} - User::pointer User::find(Session& session, UserId id) { diff --git a/src/libs/services/database/impl/Utils.cpp b/src/libs/services/database/impl/Utils.cpp index e0994090..ceb3af41 100644 --- a/src/libs/services/database/impl/Utils.cpp +++ b/src/libs/services/database/impl/Utils.cpp @@ -21,7 +21,7 @@ #include "utils/String.hpp" -namespace Database +namespace Database::Utils { std::string escapeLikeKeyword(std::string_view keyword) @@ -35,6 +35,5 @@ namespace Database // force second resolution return Wt::WDateTime::fromTime_t(dateTime.toTime_t()); } - -} // namespace Database +} // namespace Database::Utils diff --git a/src/libs/services/database/impl/Utils.hpp b/src/libs/services/database/impl/Utils.hpp index b92b12c3..213bce59 100644 --- a/src/libs/services/database/impl/Utils.hpp +++ b/src/libs/services/database/impl/Utils.hpp @@ -27,7 +27,7 @@ #include "services/database/Types.hpp" -namespace Database +namespace Database::Utils { #define ESCAPE_CHAR_STR "\\" static inline constexpr char escapeChar {'\\'}; @@ -82,5 +82,5 @@ namespace Database } Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); -} // namespace Database +} // namespace Database::Utils diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index 23b153af..04067a9c 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -72,7 +72,6 @@ class Artist : public Object }; Artist() = default; - Artist(const std::string& name, const std::optional& MBID = {}); // Accessors static std::size_t getCount(Session& session); @@ -101,9 +100,6 @@ class Artist : public Object void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setSortName(const std::string& sortName); - // Create - static pointer create(Session& session, const std::string& name, const std::optional& UUID = {}); - template void persist(Action& a) { @@ -118,6 +114,11 @@ class Artist : public Object private: static const std::size_t _maxNameLength = 128; + friend class Session; + // Create + Artist(const std::string& name, const std::optional& MBID = {}); + static pointer create(Session& session, const std::string& name, const std::optional& UUID = {}); + std::string _name; std::string _sortName; std::string _MBID; // Musicbrainz Identifier diff --git a/src/libs/services/database/include/services/database/AuthToken.hpp b/src/libs/services/database/include/services/database/AuthToken.hpp index 646a5d9f..6d784ef2 100644 --- a/src/libs/services/database/include/services/database/AuthToken.hpp +++ b/src/libs/services/database/include/services/database/AuthToken.hpp @@ -36,10 +36,8 @@ namespace Database { public: AuthToken() = default; - AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user); // Utility - static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr user); static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); static pointer find(Session& session, std::string_view value); @@ -57,6 +55,10 @@ namespace Database } private: + friend class Session; + AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user); + static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr user); + std::string _value; Wt::WDateTime _expiry; Wt::Dbo::ptr _user; diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp index a1cac3f7..72c05883 100644 --- a/src/libs/services/database/include/services/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -42,7 +42,6 @@ class Cluster : public Object { public: Cluster() = default; - Cluster(ObjectPtr type, std::string_view name); // Find utility static std::size_t getCount(Session& session); @@ -50,9 +49,6 @@ class Cluster : public Object static pointer find(Session& session, ClusterId id); static RangeResults findOrphans(Session& session, Range range); - // Create utility - static pointer create(Session& session, ObjectPtr type, std::string_view name); - // Accessors const std::string& getName() const { return _name; } ObjectPtr getType() const { return _clusterType; } @@ -72,6 +68,10 @@ class Cluster : public Object } private: + friend class Session; + Cluster(ObjectPtr type, std::string_view name); + static pointer create(Session& session, ObjectPtr type, std::string_view name); + static const std::size_t _maxNameLength = 128; std::string _name; @@ -85,7 +85,6 @@ class ClusterType : public Object { public: ClusterType() = default; - ClusterType(std::string_view name); // Getters static std::size_t getCount(Session& session); @@ -95,7 +94,6 @@ class ClusterType : public Object static RangeResults findOrphans(Session& session, Range range); static RangeResults findUsed(Session& session, Range range); - static pointer create(Session& session, const std::string& name); static void remove(Session& session, const std::string& name); // Accessors @@ -112,6 +110,10 @@ class ClusterType : public Object } private: + friend class Session; + ClusterType(std::string_view name); + static pointer create(Session& session, const std::string& name); + static const std::size_t _maxNameLength = 128; std::string _name; diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp index edbb218b..1c981b65 100644 --- a/src/libs/services/database/include/services/database/Listen.hpp +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -42,7 +42,6 @@ class Listen : public Object { public: Listen() = default; - Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); struct FindParameters { @@ -63,9 +62,6 @@ class Listen : public Object static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime); static RangeResults find(Session& session, const FindParameters& parameters); - // Create - static pointer create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); - // Stats static RangeResults getTopArtists(Session& session, UserId userId, @@ -120,6 +116,10 @@ class Listen : public Object } private: + friend class Session; + Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + static pointer create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + Wt::WDateTime _dateTime; Scrobbler _scrobbler; ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; diff --git a/src/libs/services/database/include/services/database/Object.hpp b/src/libs/services/database/include/services/database/Object.hpp index 79ccf4be..e4ea5342 100644 --- a/src/libs/services/database/include/services/database/Object.hpp +++ b/src/libs/services/database/include/services/database/Object.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include "services/database/IdType.hpp" @@ -38,7 +39,11 @@ namespace Database bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; } auto modify() { return _obj.modify(); } - void remove() { _obj.remove(); } + void remove() + { + _obj.modify()->onPreRemove(); + _obj.remove(); + } private: template friend class Object; @@ -61,6 +66,10 @@ namespace Database typename Wt::Dbo::dbo_traits::IdType id() const = delete; protected: + template friend class ObjectPtr; + virtual void onPostCreated() {} + virtual void onPreRemove() {} + // Can get raw dbo ptr only from Objects template static diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 656dc69e..0bf62e43 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -79,7 +79,6 @@ class Release : public Object }; Release() = default; - Release(const std::string& name, const std::optional& MBID = {}); // Accessors static std::size_t getCount(Session& session); @@ -98,9 +97,6 @@ class Release : public Object // size is the max number of cluster per cluster type std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - // Create - static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - // Utility functions std::optional getReleaseYear(bool originalDate = false) const; std::optional getCopyright() const; @@ -133,6 +129,10 @@ class Release : public Object } 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 = {}); + static const std::size_t _maxNameLength {128}; std::string _name; diff --git a/src/libs/services/database/include/services/database/ScanSettings.hpp b/src/libs/services/database/include/services/database/ScanSettings.hpp index 19def107..e51a6cc0 100644 --- a/src/libs/services/database/include/services/database/ScanSettings.hpp +++ b/src/libs/services/database/include/services/database/ScanSettings.hpp @@ -39,7 +39,8 @@ class ScanSettings : public Object { public: // Do not modify values (just add) - enum class UpdatePeriod { + enum class UpdatePeriod + { Never = 0, Daily, Weekly, diff --git a/src/libs/services/database/include/services/database/Session.hpp b/src/libs/services/database/include/services/database/Session.hpp index 1aab2d66..0766404f 100644 --- a/src/libs/services/database/include/services/database/Session.hpp +++ b/src/libs/services/database/include/services/database/Session.hpp @@ -19,16 +19,15 @@ #pragma once -#include - #include #include #include "utils/RecursiveSharedMutex.hpp" +#include "services/database/Object.hpp" + namespace Database { - class UniqueTransaction { private: @@ -73,11 +72,22 @@ namespace Database Wt::Dbo::Session& getDboSession() { return _session; } Db& getDb() { return _db; } + template + typename Object::pointer create(Args&&... args) + { + checkUniqueLocked(); + + typename Object::pointer res {Object::create(*this, std::forward(args)...)}; + getDboSession().flush(); + + res.modify()->onPostCreated(); + return res; + } + private: Db& _db; Wt::Dbo::Session _session; }; - } // namespace Database diff --git a/src/libs/services/database/include/services/database/StarredArtist.hpp b/src/libs/services/database/include/services/database/StarredArtist.hpp index 7c731578..3d7a0d18 100644 --- a/src/libs/services/database/include/services/database/StarredArtist.hpp +++ b/src/libs/services/database/include/services/database/StarredArtist.hpp @@ -38,16 +38,12 @@ namespace Database { public: StarredArtist() = default; - StarredArtist(ObjectPtr artist, ObjectPtr 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, ObjectPtr user, Scrobbler scrobbler); - // Accessors ObjectPtr getArtist() const { return _artist; } ObjectPtr getUser() const { return _user; } @@ -71,6 +67,10 @@ namespace Database } private: + friend class Session; + StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + static pointer create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + Scrobbler _scrobbler; // for which scrobbler ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; Wt::WDateTime _dateTime; // when it was starred diff --git a/src/libs/services/database/include/services/database/StarredRelease.hpp b/src/libs/services/database/include/services/database/StarredRelease.hpp index c79ce4b2..b53c1850 100644 --- a/src/libs/services/database/include/services/database/StarredRelease.hpp +++ b/src/libs/services/database/include/services/database/StarredRelease.hpp @@ -38,16 +38,12 @@ namespace Database { public: StarredRelease() = default; - StarredRelease(ObjectPtr release, ObjectPtr 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, ObjectPtr user, Scrobbler scrobbler); - // Accessors ObjectPtr getRelease() const { return _release; } ObjectPtr getUser() const { return _user; } @@ -71,6 +67,10 @@ namespace Database } private: + friend class Session; + StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + static pointer create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + Scrobbler _scrobbler; // for which scrobbler ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; Wt::WDateTime _dateTime; // when it was starred diff --git a/src/libs/services/database/include/services/database/StarredTrack.hpp b/src/libs/services/database/include/services/database/StarredTrack.hpp index 1332e1ae..376fd00d 100644 --- a/src/libs/services/database/include/services/database/StarredTrack.hpp +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -39,7 +39,6 @@ namespace Database { public: StarredTrack() = default; - StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); struct FindParameters { @@ -59,9 +58,6 @@ namespace Database static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler); static RangeResults find(Session& session, const FindParameters& findParams); - // Create utility - static pointer create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); - // Accessors ObjectPtr getTrack() const { return _track; } ObjectPtr getUser() const { return _user; } @@ -85,6 +81,10 @@ namespace Database } private: + friend class Session; + StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + static pointer create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + Scrobbler _scrobbler; // for which scrobbler ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; Wt::WDateTime _dateTime; // when it was starred diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index 3cc10e7c..f47d903b 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -91,7 +91,6 @@ class Track : public Object }; Track() = default; - Track(const std::filesystem::path& p); // Find utility functions static std::size_t getCount(Session& session); @@ -107,9 +106,6 @@ class Track : public Object static RangeResults findRecordingMBIDDuplicates(Session& session, Range range); static RangeResults findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); - // Create utility - static pointer create(Session& session, const std::filesystem::path& p); - // Accessors void setScanVersion(std::size_t version) { _scanVersion = version; } void setTrackNumber(int num) { _trackNumber = num; } @@ -196,6 +192,10 @@ class Track : public Object } private: + friend class ::Database::Session; + Track(const std::filesystem::path& p); + static pointer create(Session& session, const std::filesystem::path& p); + static const std::size_t _maxNameLength = 128; static const std::size_t _maxCopyrightLength = 128; static const std::size_t _maxCopyrightURLLength = 128; diff --git a/src/libs/services/database/include/services/database/TrackBookmark.hpp b/src/libs/services/database/include/services/database/TrackBookmark.hpp index d1d31768..dee9b0fa 100644 --- a/src/libs/services/database/include/services/database/TrackBookmark.hpp +++ b/src/libs/services/database/include/services/database/TrackBookmark.hpp @@ -41,10 +41,6 @@ class TrackBookmark : public Object { public: TrackBookmark () = default; - TrackBookmark(ObjectPtr user, ObjectPtr track); - - // utility - static pointer create(Session& session, ObjectPtr user, ObjectPtr track); // Find utility functions static std::size_t getCount(Session& session); @@ -70,7 +66,12 @@ class TrackBookmark : public Object Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); } + private: + friend class Session; + TrackBookmark(ObjectPtr user, ObjectPtr track); + static pointer create(Session& session, ObjectPtr user, ObjectPtr track); + static const std::size_t _maxCommentLength = 128; std::chrono::duration _offset; diff --git a/src/libs/services/database/include/services/database/TrackFeatures.hpp b/src/libs/services/database/include/services/database/TrackFeatures.hpp index 1c1251fa..b294fc72 100644 --- a/src/libs/services/database/include/services/database/TrackFeatures.hpp +++ b/src/libs/services/database/include/services/database/TrackFeatures.hpp @@ -46,7 +46,6 @@ class TrackFeatures : public Object { public: TrackFeatures() = default; - TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures); // Find utilities static std::size_t getCount(Session& session); @@ -54,9 +53,6 @@ class TrackFeatures : public Object static pointer find(Session& session, TrackId trackId); static RangeResults find(Session& session, Range range); - // Create utility - static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); - FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; @@ -71,6 +67,10 @@ class TrackFeatures : public Object } private: + friend class Session; + TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures); + static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); + std::string _data; Wt::Dbo::ptr _track; }; diff --git a/src/libs/services/database/include/services/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp index c23ef926..5eeb7d94 100644 --- a/src/libs/services/database/include/services/database/TrackList.hpp +++ b/src/libs/services/database/include/services/database/TrackList.hpp @@ -48,7 +48,6 @@ class TrackList : public Object { public: TrackList() = default; - TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); // Stats utility std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; @@ -62,20 +61,19 @@ class TrackList : public Object Range range; std::optional type; UserId user; // only tracklists owned by this user + TrackListSortMethod sortMethod {TrackListSortMethod::None}; FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setType(TrackListType _type) { type = _type; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setSortMethod(TrackListSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } }; static std::size_t getCount(Session& session); static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); static pointer find(Session& session, TrackListId tracklistId); static RangeResults find(Session& session, const FindParameters& params); - // Create utility - static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); - // Accessors std::string_view getName() const { return _name; } bool isPublic() const { return _isPublic; } @@ -86,6 +84,7 @@ class TrackList : public Object void setName(const std::string& name) { _name = name; } void setIsPublic(bool isPublic) { _isPublic = isPublic; } void clear() { _entries.clear(); } + void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { _lastModifiedDateTime = dateTime; } // Get tracks, ordered by position bool isEmpty() const; @@ -117,18 +116,26 @@ class TrackList : public Object template void persist(Action& a) { - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _isPublic, "public"); + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _isPublic, "public"); + Wt::Dbo::field(a, _creationDateTime, "creation_date_time"); + Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time"); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); } private: + friend class Session; + TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + std::string _name; TrackListType _type {TrackListType::Playlist}; bool _isPublic {false}; + Wt::WDateTime _creationDateTime; + Wt::WDateTime _lastModifiedDateTime; Wt::Dbo::ptr _user; Wt::Dbo::collection> _entries; @@ -138,15 +145,13 @@ class TrackListEntry : public Object { public: TrackListEntry() = default; - TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); - TrackListEntry(ObjectPtr track, ObjectPtr tracklist); + + void onPostCreated() override; + void onPreRemove() override; // find utility static pointer getById(Session& session, TrackListEntryId id); - // Create utility - static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); - // Accessors ObjectPtr getTrack() const { return _track; } const Wt::WDateTime& getDateTime() const { return _dateTime; } @@ -161,6 +166,11 @@ class TrackListEntry : public Object } private: + friend class Session; + TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr tracklist); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); + Wt::WDateTime _dateTime; // optional date time Wt::Dbo::ptr _track; Wt::Dbo::ptr _tracklist; diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index fe5ec604..691d040c 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -104,6 +104,13 @@ namespace Database StarredDateDesc, }; + enum class TrackListSortMethod + { + None, + Name, + LastModifiedDesc, + }; + enum class TrackSortMethod { None, diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index 4a446296..300efc67 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -64,10 +64,6 @@ class User : public Object static inline const Scrobbler defaultScrobbler {Scrobbler::Internal}; User() = default; - User(std::string_view loginName); - - // utility - static pointer create(Session& session, std::string_view loginName); static std::size_t getCount(Session& session); static pointer find(Session& session, UserId id); @@ -137,6 +133,10 @@ class User : public Object } private: + friend class Session; + User(std::string_view loginName); + static pointer create(Session& session, std::string_view loginName); + std::string _loginName; std::string _passwordSalt; std::string _passwordHash; diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index fd3c11c7..d280bfcc 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -345,7 +345,6 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks) track1.get().modify()->setRelease(release.get()); } - { auto transaction {session.createSharedTransaction()}; diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index 0added83..f3d91845 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -211,9 +211,9 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, trackA.get(), trackList.get()); - TrackListEntry::create(session, trackB.get(), trackList.get()); - TrackListEntry::create(session, trackB.get(), trackList.get()); + session.create(trackA.get(), trackList.get()); + session.create(trackB.get(), trackList.get()); + session.create(trackB.get(), trackList.get()); } { @@ -249,9 +249,9 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, trackC.get(), trackList.get()); - TrackListEntry::create(session, trackC.get(), trackList.get()); - TrackListEntry::create(session, trackC.get(), trackList.get()); + session.create(trackC.get(), trackList.get()); + session.create(trackC.get(), trackList.get()); + session.create(trackC.get(), trackList.get()); } { @@ -630,7 +630,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster) auto transaction {session.createUniqueTransaction()}; if (i < 5) - TrackListEntry::create(session, tracks.back().get(), trackList.get()); + session.create(tracks.back().get(), trackList.get()); if (i < 10) cluster.get().modify()->addTrack(tracks.back().get()); @@ -663,7 +663,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) auto transaction {session.createUniqueTransaction()}; if (i < 5) - TrackListEntry::create(session, tracks.back().get(), trackList.get()); + session.create(tracks.back().get(), trackList.get()); if (i < 10) { @@ -742,7 +742,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track1.get(), trackList.get(), now); + session.create(track1.get(), trackList.get(), now); } { @@ -828,7 +828,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1)); + session.create(track2.get(), trackList.get(), now.addSecs(1)); } { @@ -908,7 +908,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2)); + session.create(track1.get(), trackList.get(), now.addSecs(2)); } { diff --git a/src/libs/services/database/test/Common.hpp b/src/libs/services/database/test/Common.hpp index 25c47de6..5a692695 100644 --- a/src/libs/services/database/test/Common.hpp +++ b/src/libs/services/database/test/Common.hpp @@ -29,6 +29,7 @@ #include "services/database/Db.hpp" #include "services/database/Listen.hpp" #include "services/database/Release.hpp" +#include "services/database/ScanSettings.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" @@ -50,7 +51,7 @@ class ScopedEntity { auto transaction {_session.createUniqueTransaction()}; - auto entity {T::create(_session, std::forward(args)...)}; + auto entity {_session.create(std::forward(args)...)}; EXPECT_TRUE(entity); _id = entity->getId(); } @@ -60,7 +61,9 @@ class ScopedEntity auto transaction {_session.createUniqueTransaction()}; auto entity {T::find(_session, _id)}; - entity.remove(); + // could not be here due to "on delete cascade" constraints... + if (entity) + entity.remove(); } ScopedEntity(const ScopedEntity&) = delete; diff --git a/src/libs/services/database/test/TrackList.cpp b/src/libs/services/database/test/TrackList.cpp index db096dac..243caab3 100644 --- a/src/libs/services/database/test/TrackList.cpp +++ b/src/libs/services/database/test/TrackList.cpp @@ -59,7 +59,7 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track.get(), trackList1.get()); + session.create(track.get(), trackList1.get()); } { @@ -74,6 +74,53 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack) } } +TEST_F(DatabaseFixture, TrackList_SortMethod) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; + ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; + ScopedTrack track {session, "MyTrack"}; + + { + auto transaction {session.createSharedTransaction()}; + + const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name))}; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + session.create(track.get(), trackList1.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + session.create(track.get(), trackList2.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList2.getId()); + EXPECT_EQ(trackLists.results[1], trackList1.getId()); + } +} + TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) { ScopedUser user {session, "MyUser"}; @@ -85,7 +132,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) tracks.emplace_back(session, "MyTrack" + std::to_string(i)); auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, tracks.back().get(), trackList.get()); + session.create(tracks.back().get(), trackList.get()); } { @@ -127,9 +174,9 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) { 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)); + session.create(track1.get(), trackList.get(), now); + session.create(track2.get(), trackList.get(), now.addSecs(-1)); + session.create(track3.get(), trackList.get(), now.addSecs(1)); } { @@ -176,7 +223,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track1.get(), trackList.get(), now); + session.create(track1.get(), trackList.get(), now); } { @@ -198,7 +245,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1)); + session.create(track2.get(), trackList.get(), now.addSecs(1)); } { @@ -224,7 +271,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2)); + session.create(track1.get(), trackList.get(), now.addSecs(2)); } { @@ -270,7 +317,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists) { auto transaction {session.createUniqueTransaction()}; TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackListEntry::create(session, track1.get(), trackList.get()); + session.create(track1.get(), trackList.get()); } { @@ -303,7 +350,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists) { auto transaction {session.createUniqueTransaction()}; TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - TrackListEntry::create(session, track2.get(), trackList.get()); + session.create(track2.get(), trackList.get()); artist1.get().modify()->setSortName("ZZZ"); artist2.get().modify()->setSortName("AAA"); } @@ -350,7 +397,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getReleases) track.get().modify()->setRelease(release.get()); - TrackListEntry::create(session, track.get(), trackList.get()); + session.create(track.get(), trackList.get()); } { @@ -382,9 +429,9 @@ TEST_F(DatabaseFixture, SingleTrackList_getTracks) { 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()); + session.create(track1.get(), trackList.get()); + session.create(track2.get(), trackList.get()); + session.create(track1.get(), trackList.get()); } { @@ -418,7 +465,7 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) { auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track.get(), trackList1.get()); + session.create(track.get(), trackList1.get()); cluster.get().modify()->addTrack(track.get()); } diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index fc712c13..c970b9d2 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -100,7 +100,7 @@ static Artist::pointer createArtist(Session& session, const MetaData::Artist& artistInfo) { - Artist::pointer artist {Artist::create(session, artistInfo.name)}; + Artist::pointer artist {session.create(artistInfo.name)}; if (artistInfo.musicBrainzArtistID) artist.modify()->setMBID(*artistInfo.musicBrainzArtistID); @@ -187,7 +187,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) release = Release::find(session, *album.musicBrainzAlbumID); if (!release) { - release = Release::create(session, album.name, album.musicBrainzAlbumID); + release = session.create(album.name, album.musicBrainzAlbumID); } else if (release->getName() != album.name) { @@ -213,7 +213,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) // No release found with the same name and without MBID -> creating if (!release) - release = Release::create(session, album.name); + release = session.create(album.name); return release; } @@ -236,7 +236,7 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) { auto cluster = clusterType->getCluster(clusterName); if (!cluster) - cluster = Cluster::create(session, clusterType, clusterName); + cluster = session.create(clusterType, clusterName); clusters.push_back(cluster); } @@ -567,7 +567,7 @@ ScannerService::fetchTrackFeatures(TrackId trackId, const UUID& recordingMBID) if (!track) return false; - TrackFeatures::create(_dbSession, track, data); + _dbSession.create(track, data); } return true; @@ -788,7 +788,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, if (!track) { // Create a new song - track = Track::create(_dbSession, file); + track = _dbSession.create(file); LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'"; stats.additions++; } diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp index be0a06ba..9514c1f1 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp @@ -51,7 +51,7 @@ namespace Scrobbling if (!user) return; - starredObj = StarredObjType::create(session, obj, user, *scrobbler); + starredObj = session.create(obj, user, *scrobbler); } starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); starredObjId = starredObj->getId(); diff --git a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp index a4d8cedc..775ef943 100644 --- a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp +++ b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp @@ -88,7 +88,7 @@ namespace Scrobbling if (!track) return; - auto dbListen {Database::Listen::create(session, user, track, Database::Scrobbler::Internal, listen.listenedAt)}; + auto dbListen {session.create(user, track, Database::Scrobbler::Internal, listen.listenedAt)}; dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); } diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp index 7357ce00..22f8be18 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp @@ -574,7 +574,7 @@ namespace Scrobbling::ListenBrainz if (!user) return; - StarredTrack::pointer starredTrack {StarredTrack::create(session, track, user, Database::Scrobbler::ListenBrainz)}; + StarredTrack::pointer starredTrack {session.create(track, user, Database::Scrobbler::ListenBrainz)}; starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized); context.importedFeedbackCount++; diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index 11316b6b..79510265 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -366,7 +366,7 @@ namespace Scrobbling::ListenBrainz if (!track) return false; - dbListen = Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); + dbListen = session.create(user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); dbListen.modify()->setScrobblingState(scrobblingState); LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 4ec3ec92..aa283e98 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -563,7 +563,7 @@ handleCreatePlaylistRequest(RequestContext& context) } else { - tracklist = TrackList::create(context.dbSession, *name, TrackListType::Playlist, false, user); + tracklist = context.dbSession.create(*name, TrackListType::Playlist, false, user); } for (const TrackId trackId : trackIds) @@ -572,7 +572,7 @@ handleCreatePlaylistRequest(RequestContext& context) if (!track) continue; - TrackListEntry::create(context.dbSession, track, tracklist); + context.dbSession.create(track, tracklist); } return Response::createOkResponse(context.serverProtocolVersion); @@ -594,7 +594,7 @@ handleCreateUserRequest(RequestContext& context) if (user) throw UserAlreadyExistsGenericError {}; - user = User::create(context.dbSession, username); + user = context.dbSession.create(username); userId = user->getId(); } @@ -1699,7 +1699,7 @@ handleUpdatePlaylistRequest(RequestContext& context) if (!track) continue; - TrackListEntry::create(context.dbSession, track, tracklist); + context.dbSession.create(track, tracklist); } return Response::createOkResponse(context.serverProtocolVersion); @@ -1754,7 +1754,7 @@ handleCreateBookmark(RequestContext& context) // Replace any existing bookmark auto bookmark {TrackBookmark::find(context.dbSession, user->getId(), trackId)}; if (!bookmark) - bookmark = TrackBookmark::create(context.dbSession, user, track); + bookmark = context.dbSession.create(user, track); bookmark.modify()->setOffset(std::chrono::milliseconds {position}); if (comment) diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 78f7f892..b627fe9f 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable(lms ui/explore/SearchView.cpp ui/explore/TrackCollector.cpp ui/explore/TrackListHelpers.cpp + ui/explore/TrackListView.cpp ui/explore/TrackListsView.cpp ui/explore/TracksView.cpp ui/resource/AudioFileResource.cpp diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 2145088f..9fd4527a 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -34,6 +34,7 @@ #include "services/database/Db.hpp" #include "services/database/Release.hpp" #include "services/database/Session.hpp" +#include "services/database/TrackList.hpp" #include "services/database/User.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Logger.hpp" @@ -191,6 +192,7 @@ LmsApplication::init() messageResourceBundle().use(appRoot() + "releases"); messageResourceBundle().use(appRoot() + "search"); messageResourceBundle().use(appRoot() + "settings"); + messageResourceBundle().use(appRoot() + "tracklist"); messageResourceBundle().use(appRoot() + "tracklists"); messageResourceBundle().use(appRoot() + "tracks"); @@ -300,6 +302,23 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add return res; } +std::unique_ptr +LmsApplication::createTrackListAnchor(Database::TrackList::pointer trackList, bool addText) +{ + Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()}; + auto res {std::make_unique(link)}; + + if (addText) + { + const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})}; + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(name); + res->setToolTip(name, Wt::TextFormat::Plain); + } + + return res; +} + void LmsApplication::handleException(LmsApplicationException& e) { @@ -350,6 +369,7 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) { "/search", IdxExplore, false }, { "/tracks", IdxExplore, false }, { "/tracklists", IdxExplore, false }, + { "/tracklist", IdxExplore, false }, { "/playqueue", IdxPlayQueue, false }, { "/settings", IdxSettings, false }, { "/admin/database", IdxAdminDatabase, true }, @@ -546,7 +566,7 @@ LmsApplication::createHome() }); } - internalPathChanged().connect([=] + internalPathChanged().connect(mainStack, [=] { handlePathChange(*mainStack, isAdmin); }); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index af89b8f7..17619d40 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -36,6 +36,7 @@ namespace Database class Db; class Release; class Session; + class TrackList; class User; } @@ -82,6 +83,7 @@ class LmsApplication : public Wt::WApplication static std::unique_ptr createArtistAnchor(Database::ObjectPtr artist, bool addText = true); static Wt::WLink createReleaseLink(Database::ObjectPtr release); static std::unique_ptr createReleaseAnchor(Database::ObjectPtr release, bool addText = true); + static std::unique_ptr createTrackListAnchor(Database::ObjectPtr trackList, bool addText = true); MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; } PlayQueue& getPlayQueue() const { return *_playQueue; } diff --git a/src/lms/ui/LmsApplicationException.hpp b/src/lms/ui/LmsApplicationException.hpp index 463bd899..f5ef4691 100644 --- a/src/lms/ui/LmsApplicationException.hpp +++ b/src/lms/ui/LmsApplicationException.hpp @@ -42,6 +42,12 @@ namespace UserInterface ReleaseNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.release-not-found")} {} }; + class TrackListNotFoundException : public LmsApplicationException + { + public: + TrackListNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.tracklist-not-found")} {} + }; + class UserNotFoundException : public LmsApplicationException { public: diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 08534c0c..58a481ff 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -74,7 +74,7 @@ PlayQueue::PlayQueue() getTrackList().modify()->clear(); for (const auto& entry : entries) - Database::TrackListEntry::create(LmsApp->getDbSession(), entry->getTrack(), trackList); + LmsApp->getDbSession().create(entry->getTrack(), trackList); } _entriesContainer->clear(); addSome(); @@ -150,12 +150,12 @@ PlayQueue::PlayQueue() static const std::string queuedListName {"__queued_tracks__"}; trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackListType::Internal, LmsApp->getUserId()); if (!trackList) - trackList = Database::TrackList::create(LmsApp->getDbSession(), queuedListName, Database::TrackListType::Internal, false, LmsApp->getUser()); + trackList = LmsApp->getDbSession().create(queuedListName, Database::TrackListType::Internal, false, LmsApp->getUser()); } else { static const std::string currentPlayQueueName {"__current__playqueue__"}; - trackList = Database::TrackList::create(LmsApp->getDbSession(), currentPlayQueueName, Database::TrackListType::Internal, false, LmsApp->getUser()); + trackList = LmsApp->getDbSession().create(currentPlayQueueName, Database::TrackListType::Internal, false, LmsApp->getUser()); } _tracklistId = trackList->getId(); @@ -323,7 +323,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) if (nbTracksQueued == nbTracksToEnqueue) break; - Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist); + LmsApp->getDbSession().create(track, tracklist); nbTracksQueued++; } } diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index 4f4316dd..98d96f64 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -67,7 +67,7 @@ class InitWizardModel : public Wt::WFormModel if (Database::User::getCount(LmsApp->getDbSession()) > 0) throw LmsException {"Admin user already created"}; - Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; + Database::User::pointer user {LmsApp->getDbSession().create(valueText(AdminLoginField).toUTF8())}; user.modify()->setType(Database::UserType::ADMIN); Service<::Auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8()); } diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index fa5d50ae..99f41e6c 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -96,7 +96,7 @@ class UserModel : public Wt::WFormModel throw UserNotAllowedException {}; // Create user - user = User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); + user = LmsApp->getDbSession().create(valueText(LoginField).toUTF8()); if (Wt::asNumber(value(DemoField))) user.modify()->setType(UserType::DEMO); diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index d5b3a850..c1b00ddd 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -37,6 +37,7 @@ #include "ReleasesView.hpp" #include "ReleaseView.hpp" #include "SearchView.hpp" +#include "TrackListView.hpp" #include "TrackListsView.hpp" #include "TracksView.hpp" @@ -52,6 +53,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack) IdxArtists = 0, IdxArtist, IdxTracklists, + IdxTracklist, IdxReleases, IdxRelease, IdxSearch, @@ -63,6 +65,7 @@ handleContentsPathChange(Wt::WStackedWidget* stack) { "/artists", IdxArtists }, { "/artist", IdxArtist }, { "/tracklists", IdxTracklists }, + { "/tracklist", IdxTracklist }, { "/releases", IdxReleases }, { "/release", IdxRelease }, { "/search", IdxSearch }, @@ -104,6 +107,11 @@ Explore::Explore(Filters* filters) tracklists->trackListAction.connect(this, &Explore::handleTrackListAction); contentsStack->addWidget(std::move(tracklists)); + auto tracklist {std::make_unique(*_filters)}; + tracklist->trackListAction.connect(this, &Explore::handleTrackListAction); + tracklist->tracksAction.connect(this, &Explore::handleTracksAction); + contentsStack->addWidget(std::move(tracklist)); + auto releases = std::make_unique(*_filters); releases->releasesAction.connect(this, &Explore::handleReleasesAction); contentsStack->addWidget(std::move(releases)); diff --git a/src/lms/ui/explore/TrackListView.cpp b/src/lms/ui/explore/TrackListView.cpp new file mode 100644 index 00000000..5ebf7d0b --- /dev/null +++ b/src/lms/ui/explore/TrackListView.cpp @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2022 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "TrackListView.hpp" + +#include + +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "utils/Logger.hpp" +#include "utils/String.hpp" + +#include "common/InfiniteScrollingContainer.hpp" +#include "resource/DownloadResource.hpp" +#include "Filters.hpp" +#include "LmsApplication.hpp" +#include "LmsApplicationException.hpp" +#include "ModalManager.hpp" +#include "TrackListHelpers.hpp" +#include "Utils.hpp" + +using namespace Database; + +namespace +{ + static + std::optional + extractTrackListIdFromInternalPath() + { + return StringUtils::readAs(wApp->internalPathNextPart("/tracklist/")); + } +} + +namespace UserInterface +{ + TrackList::TrackList(Filters& filters) + : Template {Wt::WString::tr("Lms.Explore.TrackList.template")} + { + addFunction("tr", &Wt::WTemplate::Functions::tr); + addFunction("id", &Wt::WTemplate::Functions::id); + + wApp->internalPathChanged().connect(this, [this] + { + refreshView(); + }); + + filters.updated().connect([this] + { + refreshView(); + }); + + refreshView(); + } + + void + TrackList::refreshView() + { + if (!wApp->internalPathMatches("/tracklist/")) + return; + + const std::optional trackListId {extractTrackListIdFromInternalPath()}; + if (!trackListId) + throw TrackListNotFoundException {}; + + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)}; + if (!trackList) + throw TrackListNotFoundException {}; + + _trackListId = *trackListId; + + clear(); + + bindString("name", std::string {trackList->getName()}, Wt::TextFormat::Plain); + bindString("duration", Utils::durationToString(trackList->getDuration())); + bindString("track-count", Wt::WString::trn("Lms.Explore.TrackList.track-count", trackList->getCount()).arg(trackList->getCount())); + + bindNew("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) + ->clicked().connect([=] + { + trackListAction.emit(PlayQueueAction::Play, *trackListId); + }); + + bindNew("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) + ->clicked().connect([=] + { + trackListAction.emit(PlayQueueAction::PlayShuffled, *trackListId); + }); + + bindNew("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain) + ->clicked().connect([=] + { + trackListAction.emit(PlayQueueAction::PlayLast, *trackListId); + }); + + bindNew("download", Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink {std::make_unique(*trackListId)}); + + bindNew("delete", Wt::WString::tr("Lms.delete")) + ->clicked().connect([=] + { + auto modal {std::make_unique(Wt::WString::tr("Lms.Explore.TrackList.template.delete-tracklist"))}; + modal->addFunction("tr", &Wt::WTemplate::Functions::tr); + Wt::WWidget* modalPtr {modal.get()}; + + auto* delBtn {modal->bindNew("del-btn", Wt::WString::tr("Lms.delete"))}; + delBtn->clicked().connect([=] + { + { + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + + Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)}; + if (trackList) + trackList.remove(); + } + + clear(); + LmsApp->setInternalPath("/tracklists", true); + + LmsApp->getModalManager().dispose(modalPtr); + }); + + auto* cancelBtn {modal->bindNew("cancel-btn", Wt::WString::tr("Lms.cancel"))}; + cancelBtn->clicked().connect([=] + { + LmsApp->getModalManager().dispose(modalPtr); + }); + + LmsApp->getModalManager().show(std::move(modal)); + }); + + _container = bindNew("tracks"); + _container->onRequestElements.connect([this] + { + addSome(); + }); + + addSome(); + } + + void + TrackList::addSome() + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + Database::Track::FindParameters params; + params.setTrackList(_trackListId); + params.setSortMethod(Database::TrackSortMethod::TrackList); + params.setRange({static_cast(_container->getCount()), _batchSize}); + + const auto trackIds {Database::Track::find(LmsApp->getDbSession(), params)}; + for (const TrackId trackId : trackIds.results) + { + if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}) + _container->add(TrackListHelpers::createEntry(track, tracksAction)); + } + + _container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount); + } + +/* + + void +Tracks::addSome() +{ +} + + std::vector +Tracks::getAllTracks() +{ + RangeResults trackIds {_trackCollector.get(Range {})}; + + return std::move(trackIds.results); +} +*/ + +} // namespace UserInterface + diff --git a/src/lms/ui/explore/TrackListView.hpp b/src/lms/ui/explore/TrackListView.hpp new file mode 100644 index 00000000..fa5e8a9b --- /dev/null +++ b/src/lms/ui/explore/TrackListView.hpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/TrackListId.hpp" +#include "services/database/Types.hpp" + +#include "common/Template.hpp" +#include "PlayQueueAction.hpp" + +namespace UserInterface +{ + class Filters; + class InfiniteScrollingContainer; + + class TrackList : public Template + { + public: + TrackList(Filters& filters); + + PlayQueueActionTrackListSignal trackListAction; + PlayQueueActionTrackSignal tracksAction; + + private: + void refreshView(); + void addSome(); + + static constexpr std::size_t _batchSize {6}; + static constexpr std::size_t _maxCount {8000}; + + Database::TrackListId _trackListId; + InfiniteScrollingContainer* _container {}; + }; +} // namespace UserInterface + diff --git a/src/lms/ui/explore/TrackListsView.cpp b/src/lms/ui/explore/TrackListsView.cpp index 90623558..5216d194 100644 --- a/src/lms/ui/explore/TrackListsView.cpp +++ b/src/lms/ui/explore/TrackListsView.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 Emeric Poupon + * Copyright (C) 2022 Emeric Poupon * * This file is part of LMS. * @@ -26,11 +26,8 @@ #include "common/InfiniteScrollingContainer.hpp" #include "common/Template.hpp" -#include "resource/DownloadResource.hpp" -#include "ReleaseListHelpers.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" -#include "ModalManager.hpp" #include "Utils.hpp" using namespace Database; @@ -45,11 +42,33 @@ namespace UserInterface addFunction("tr", &Wt::WTemplate::Functions::tr); addFunction("id", &Wt::WTemplate::Functions::id); + auto bindMenuItem {[this](const std::string& var, const Wt::WString& title, Mode mode) + { + auto *menuItem {bindNew(var, title)}; + menuItem->clicked().connect([=] + { + _mode = mode; + refreshView(); + _currentActiveItem->removeStyleClass("active"); + menuItem->addStyleClass("active"); + _currentActiveItem = menuItem; + }); + + if (mode == _mode) + { + _currentActiveItem = menuItem; + _currentActiveItem->addStyleClass("active"); + } + }}; + + bindMenuItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), Mode::RecentlyModified); + bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), Mode::All); + _container = bindNew("tracklists", Wt::WString::tr("Lms.Explore.TrackLists.template.container")); _container->onRequestElements.connect([this] - { - addSome(); - }); + { + addSome(); + }); _filters.updated().connect([this] { @@ -79,6 +98,15 @@ namespace UserInterface params.setUser(LmsApp->getUserId()); params.setType(TrackListType::Playlist); params.setRange(range); + switch (_mode) + { + case Mode::All: + params.setSortMethod(TrackListSortMethod::Name); + break; + case Mode::RecentlyModified: + params.setSortMethod(TrackListSortMethod::LastModifiedDesc); + break; + } const auto trackListIds {TrackList::find(session, params)}; for (const TrackListId trackListId : trackListIds.results) @@ -94,66 +122,9 @@ namespace UserInterface TrackLists::addTracklist(const ObjectPtr& trackList) { const TrackListId trackListId {trackList->getId()}; - Template* entry {_container->addNew