diff --git a/INSTALL.md b/INSTALL.md
index c8f44f61..4be9decd 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -50,7 +50,7 @@ git clone https://github.com/epoupon/lms.git lms
cd lms
mkdir build
cd build
-cmake .. -DCMAKE_BUILD_TYPE=Release
+cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
```
__Notes__:
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`).
@@ -106,7 +106,7 @@ You can define which authentication backend to be used thanks to the `authentica
* `PAM`: the user/password authentication request is forwarded to PAM (see the default [PAM configuration file](conf/pam/lms) provided).
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option.
__Note__: the first created user is the admin user
-### `internal` backend: reset admin password
+#### `internal` backend: reset admin password
Open the the database file located in `/var/lms/lms.db` using `sqlite3`:
```sh
sqlite3 /var/lms/lms.db
diff --git a/README.md b/README.md
index 2c1302a5..628ed696 100644
--- a/README.md
+++ b/README.md
@@ -12,16 +12,12 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* Audio transcode for maximum interoperability and low bandwith requirements
* Multi-value tags: artists, genres, composers, lyricists, moods, ...
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
-* [ListenBrainz](https://listenbrainz.org) support for scrobbling and synchronizing listens
-* Compilation support
-* Disc subtitles support
+* [ListenBrainz](https://listenbrainz.org) support for:
+ * Scrobbling and synchronizing listens
+ * Synchronizing 'love' feedbacks
* ReplayGain support
-* Persistent play queue across sessions
-* _Systemd_ integration
* User management, with several authentication backends, see [Deployment](INSTALL.md#deployment)
-* Subsonic API, with the following additional features:
- * Playlists
- * Bookmarks
+* Subsonic API
## Music discovery
_LMS_ provides several ways to help you find the music you like:
@@ -49,9 +45,7 @@ __Notes on the self-organizing map__:
## Subsonic API
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_ and _DSub_.
-
-Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection when using the directory browsing commands.
-
+Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.
The Subsonic API is enabled by default.
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
@@ -79,16 +73,14 @@ $setmulti(albumartistssort,%_albumartists_sort%)
## Security considerations
_Wt_ (the web framework used) has some [built-in security measures](https://www.webtoolkit.eu/wt/features#security), but _LMS_ also has some too:
-* to mitigate brute force login attempts, _LMS_ uses an internal login throttler based on the client IP address. The `Client-IP` or `X-Forwarded-For` headers are used to determined the real IP adress, so make sure to properly configure your reverse proxy to filter or even erase the values (see example in [INSTALL.md](INSTALL.md)).
+* to mitigate brute force login attempts, _LMS_ uses an internal login throttler based on the client IP address. The `Client-IP` or `X-Forwarded-For` headers are used to determine the real IP adress, so make sure to properly configure your reverse proxy to filter or even erase the values (see example in [INSTALL.md](INSTALL.md)).
* all passwords are stored hashed and salted using [bcrypt](https://fr.wikipedia.org/wiki/Bcrypt)
-* all the resources relative to the music collection (tracks, covers, etc.) are private to a session
+* all the resources relative to the music collection (tracks, covers, etc.) are private to an anthenticated session
## Installation
-
See [INSTALL.md](INSTALL.md) file.
## Contributing
-
Any feedback is welcome:
* feel free to participate in [discussions](https://github.com/epoupon/lms/discussions) if you have questions,
* report any bug or request for new features in the [issue tracker](https://github.com/epoupon/lms/issues),
diff --git a/approot/admin-users.xml b/approot/admin-users.xml
index ac326d97..ec9417f6 100644
--- a/approot/admin-users.xml
+++ b/approot/admin-users.xml
@@ -14,9 +14,12 @@
${name}${}${tag} ${ }
+ ${}
- ${}${edit-btn class="btn btn-sm btn-secondary me-1"}${del-btn class="btn btn-sm btn-danger"}${ }
+ ${edit-btn class="btn btn-sm btn-outline-secondary border-0"}
+ ${del-btn class="btn btn-sm btn-outline-danger border-0"}
+ ${ }
@@ -25,12 +28,9 @@
-
- ${tr:Lms.Admin.Users.del-user-name}
-
diff --git a/approot/artist.xml b/approot/artist.xml
index 2b3f5730..67d65f0a 100644
--- a/approot/artist.xml
+++ b/approot/artist.xml
@@ -4,33 +4,37 @@
-
${name}
+
${name}
${clusters class="mb-2"}
${play-btn class="btn btn-primary"}
- ${}
+ ${}
+ ${}${tr:Lms.Explore.releases} ${ }
${releases class="mb-3"}
- ${ }
- ${}
+ ${}
+ ${}
+ ${}${tr:Lms.Explore.Artist.appears-on} ${ }
+ ${appears-on-releases class="mb-3"}
+ ${ }
+ ${}
+ ${}${tr:Lms.Explore.tracks} ${ }
${tracks class="mb-3"}
- ${ }
+ ${}
${}
-
-
${tr:Lms.Explore.Artist.similar-artists}
- ${similar-artists class="d-grid gap-3"}
-
+ ${tr:Lms.Explore.Artist.similar-artists}
+ ${similar-artists class="d-grid gap-3"}
${ }
diff --git a/approot/explore.xml b/approot/explore.xml
index 60628d1e..36eef544 100644
--- a/approot/explore.xml
+++ b/approot/explore.xml
@@ -5,9 +5,6 @@
${contents}
-
-
-
${add-filter class="btn btn-sm btn-outline-primary me-2"}${clusters class="d-inline-block align-items-center"}
@@ -35,9 +32,9 @@
${value class="form-control"}
-
+
+
diff --git a/approot/main.xml b/approot/main.xml
index ffd58fbb..5947b3db 100644
--- a/approot/main.xml
+++ b/approot/main.xml
@@ -14,9 +14,9 @@
-
+
@@ -29,16 +29,21 @@
${title class="d-block text-truncate"}
${release class="text-decoration-none text-truncate link-success"}
-
+
+
+ -- / --
+
+
-
-
-
-- /
--
+
+ ${playqueue-btn class="btn btn-outline-primary border-0"}
+
+
diff --git a/approot/messages.xml b/approot/messages.xml
index 0acf1da1..bb07bcc6 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -6,9 +6,11 @@
Add
Administration
Apply
-
Discard
Cancel
Create
+
Delete
+
Discard
+
Edit
Loading...
Login
Logout
@@ -24,6 +26,10 @@
Passwords don't match
Another session has been open. Reopen this one?
Save
+
+ {1} track
+ {1} tracks
+
User
@@ -31,6 +37,7 @@
Error occured!
Go home
Release not found
+
Playlist not found
You are not allowed to perform this operation
User not found
@@ -85,11 +92,8 @@
New user
Admin
-
Delete
-
Delete user
-
Delete user?
+
Delete user?
Demo
-
Edit
Users
@@ -126,10 +130,12 @@
Play shuffled
Random
Recently added
+
Recently modified
Recently played
Albums
Star
Starred
+
Playlists
Tracks
Type
Unstar
@@ -137,6 +143,7 @@
Various artists
+
Appears on
Similar artists
@@ -154,6 +161,9 @@
Copyright
Disc {1}
+
+
Delete playlist?
+
Search...
@@ -162,7 +172,7 @@
Clear
-
{1} tracks
+
Create new playlist
Added {1} track
Added {1} tracks
@@ -171,6 +181,7 @@
Play Queue full!
Radio mode
Repeat
+Replace existing playlist
Shuffle
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index b4572718..8d6a1b38 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -6,9 +6,11 @@
Ajouter
Administration
Appliquer
-Annuler
Annuler
Créer
+Supprimer
+Annuler
+Editer
Chargement...
Login
Quitter
@@ -24,6 +26,10 @@
Les mots de passe ne correspondent pas
Une autre session a été ouverte. Reouvrir celle-ci ?
Sauvegarder
+
+ {1} piste
+ {1} pistes
+
Utilisateur
@@ -31,6 +37,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
@@ -85,11 +92,8 @@
Ajouter
Admin
-Supprimer
-Supprimer l'utilisateur
-Supprimer l'utilisateur ?
+Supprimer l'utilisateur ?
Demo
-Editer
Utilisateurs
@@ -126,10 +130,12 @@
Jouer aléatoirement
Aléatoire
Ajoutés récemment
+Modifiés récemment
Joués récemment
Albums
Ajouter aux favoris
Favoris
+Playlists
Pistes
Type
Retirer des favoris
@@ -137,6 +143,7 @@
Artistes divers
+Apparaît dans
Artistes similaires
@@ -154,6 +161,9 @@
Copyright
Disque {1}
+
+Supprimer la liste de lecture ?
+
Rechercher...
@@ -162,7 +172,7 @@
Effacer
-{1} pistes
+Créer une nouvelle liste de lecture
{1} piste ajoutée
{1} pistes ajoutées
@@ -171,12 +181,12 @@
Liste de lecture pleine!
Mode radio
Repéter
+Remplacer une liste de lecture existante
Mélanger
Historique de lecture
-
Audio
Ces paramètres audio sont locaux à votre navigateur !
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index b8648405..4a254842 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -6,9 +6,11 @@
Aggiungi
Amministrazione
Applica
-Annulla
Annulla
Crea
+Elimina
+Annulla
+Modifica
Caricamento...
Login
Logout
@@ -24,6 +26,10 @@
Le password non corrispondono
Un'altra sessione è stata aperta. Vuoi riaprire questa?
Salva
+
+
+
+
Utente
@@ -31,6 +37,7 @@
C'è stato un errore!
Ritorna alla home
Album non trovato
+
Non sei autorizzato ad eseguire questa operazione
Utente non trovato
@@ -43,6 +50,7 @@
Mai
Cartella dei file multimediali
+
Modalità di raccomandazione brani consigliati
Basata sui tag
Basata sull'analisi acustica
@@ -84,11 +92,8 @@
Nuovo utente
Amministratore
-Elimina
-Elimina l'utente
-Vuoi eliminare l'utente?
+Elimina l'utente ?
Demo
-Modifica
Utenti
@@ -125,10 +130,12 @@
Aggiungi alla coda di riproduzione mischiando
Casuali
Aggiunti di recente
+
Riprodotti di recente
Album
Aggiungi ai preferiti
Preferiti
+
Tracce
Tipo
Rimuovi dai preferiti
@@ -136,6 +143,7 @@
Vari artisti
+
Artisti simili
@@ -153,6 +161,9 @@
Copyright
Disco {1}
+
+
+
Ricerca...
@@ -161,7 +172,7 @@
Cancella
-{1} tracce
+
Aggiunta {1} traccia
Aggiunte {1} tracce
@@ -170,12 +181,12 @@
Coda di riproduzione piena!
Modalità radio
Ripeti
+
Mischia
Storico riproduzione
-
Audio
Queste impostazioni sono salvate localmente su questo dispositivo!
@@ -191,6 +202,11 @@
Album
Preamplificazione ReplayGain
Preamplificazione ReplayGain (se non è disponibile nessuna informazione)
+
+
+
+
+
Modalità di elencazione artisti
Tutti gli artisti
Artisti album
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index fca04e46..86598f6c 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -6,9 +6,11 @@
添加
管理
应用
-撤销
取消
新建
+删除
+撤销
+编辑
加载中...
登陆
注销
@@ -24,6 +26,10 @@
密码不匹配
另一个会话已打开,重新打开这一个?
保存
+
+
+
+
用户
@@ -31,6 +37,7 @@
发生错误!
返回主页
发布未找到
+
不允许执行此操作
未找到此用户
@@ -85,11 +92,8 @@
新建用户
管理员
-删除
-删除用户
-确认要删除此用户?
+
演示
-编辑
用户
@@ -126,10 +130,12 @@
随机播放
随机
最近添加
+
最近播放
专辑
收藏
已收藏
+
音轨
类型
取消收藏
@@ -137,6 +143,7 @@
群星
+
相似歌手
@@ -154,6 +161,9 @@
版权所有
唱片 {1}
+
+
+
搜索中...
@@ -162,7 +172,7 @@
清除
-{1} 音轨
+
添加 {1} 音轨
已添加 {1} 音轨
@@ -171,6 +181,7 @@
播放队列已满!
广播模式
循环播放
+
随机播放
@@ -191,6 +202,11 @@
专辑
ReplayGain 放大器
ReplayGain 放大器 (如果没有信息)
+
+
+
+
+
歌手列表模式
所有歌手
专辑歌手
diff --git a/approot/misc.xml b/approot/misc.xml
index 195c305f..92bb948d 100644
--- a/approot/misc.xml
+++ b/approot/misc.xml
@@ -9,9 +9,14 @@
-
+
${elements}
${loading-indicator}
+
+
+
+
+
diff --git a/approot/playqueue.xml b/approot/playqueue.xml
index 55d9c47e..c10a3c15 100644
--- a/approot/playqueue.xml
+++ b/approot/playqueue.xml
@@ -6,30 +6,30 @@
+
+ ${track-count} · ${duration}
-
- ${clear-btn class="btn btn-secondary"}
-
-
- ${shuffle-btn class="btn btn-secondary"}
-
${repeat-btn class="btn-check"}
${tr:Lms.PlayQueue.template.repeat-btn}
${radio-btn class="btn-check"}
${tr:Lms.PlayQueue.template.radio-btn}
- ${nb-tracks}
+
+ ${save-btn class="btn btn-secondary"}
+
+
+ ${shuffle-btn class="btn btn-secondary"}
+
+
+ ${clear-btn class="btn btn-warning"}
+
${entries}
-
-
-
-
@@ -44,14 +44,14 @@
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
- ${del-btn class="btn btn-sm btn-outline-secondary border-0"}
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
+ ${del-btn class="btn btn-sm btn-outline-secondary border-0"}
@@ -60,4 +60,49 @@
${artist}
+
+
+
+
+
+ ${name class="form-control"}
+
+ ${name-info}
+
+
+
+
+ ${name class="form-control"}
+
+ ${name-info}
+
+
+
diff --git a/approot/release.xml b/approot/release.xml
index d964b8c2..944437db 100644
--- a/approot/release.xml
+++ b/approot/release.xml
@@ -12,7 +12,7 @@
${cover}
-
${name}
+
${name}
${
}${artists}${ }
${
}${tr:Lms.Explore.various-artists}
${ }
${}${year}${} (${orig-year})${ } · ${ }${duration}
@@ -21,13 +21,13 @@
${play-btn class="btn btn-primary"}
@@ -74,9 +74,9 @@
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
diff --git a/approot/releases.xml b/approot/releases.xml
index 859c8735..c6968a68 100644
--- a/approot/releases.xml
+++ b/approot/releases.xml
@@ -28,8 +28,8 @@
${play-btn class="btn btn-primary"}
diff --git a/approot/tracklist.xml b/approot/tracklist.xml
new file mode 100644
index 00000000..bb1af88b
--- /dev/null
+++ b/approot/tracklist.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
${name}
+
${track-count} · ${duration}
+ ${clusters class="mb-2"}
+
+ ${play-btn class="btn btn-primary"}
+
+
+
+
+ ${tracks}
+
+
+
+
+
+
+
diff --git a/approot/tracklists.xml b/approot/tracklists.xml
new file mode 100644
index 00000000..7bb0549a
--- /dev/null
+++ b/approot/tracklists.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+ ${recently-modified class="nav-link"}
+
+
+ ${all class="nav-link"}
+
+
+
+ ${tracklists}
+
+
+
+ ${name class="text-decoration-none link-secondary"}
+
+
+
+ ${elements class="d-grid gap-3"}
+ ${loading-indicator}
+
+
+
diff --git a/approot/tracks.xml b/approot/tracks.xml
index 38e6f482..60a9bac2 100644
--- a/approot/tracks.xml
+++ b/approot/tracks.xml
@@ -29,8 +29,8 @@
${play-btn class="btn btn-primary"}
@@ -54,9 +54,9 @@
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
diff --git a/conf/lms.conf b/conf/lms.conf
index b44af624..ca7dae59 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -36,10 +36,14 @@ http-server-thread-count = 0;
# ListenBrainz root API
listenbrainz-api-base-url = "https://api.listenbrainz.org";
-# How many listens to retrieve when syncing (0 disables sync)
+# How many listens to retrieve when syncing (0 to disable sync)
listenbrainz-max-sync-listen-count = 1000;
-# How often to resync listens (0 disables sync)
+# How often to resync listens (0 to disable sync)
listenbrainz-sync-listens-period-hours = 1;
+# How many feedbacks to retrieve when syncing (0 to disables sync)
+listenbrainz-max-sync-feedback-count = 1000;
+# How often to resync feedbacks (0 to disable sync)
+listenbrainz-sync-feedbacks-period-hours = 1;
# Acousticbrainz root API
acousticbrainz-api-base-url = "https://acousticbrainz.org";
@@ -71,5 +75,8 @@ cover-max-cache-size = 30;
# JPEG quality for covers (range is 1-100)
cover-jpeg-quality = 75;
+# Preferred file names for covers (order is important)
+cover-preferred-file-names = ("cover", "front" );
+
# Set to true if you want to hide duplicate tracks
scanner-skip-duplicate-recording-mbid = false;
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/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp
index a9c2b8e4..86526dab 100644
--- a/src/libs/services/cover/impl/CoverService.cpp
+++ b/src/libs/services/cover/impl/CoverService.cpp
@@ -31,6 +31,7 @@
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Random.hpp"
+#include "utils/String.hpp"
#include "utils/Utils.hpp"
namespace
@@ -68,6 +69,19 @@ namespace
return res;
}
+
+ std::vector constructPreferredFileNames()
+ {
+ std::vector res;
+
+ Service::get()->visitStrings("cover-preferred-file-names",
+ [&res](std::string_view fileName)
+ {
+ res.emplace_back(fileName);
+ }, {"cover", "front"});
+
+ return res;
+ }
}
namespace Cover {
@@ -94,6 +108,7 @@ CoverService::CoverService(Database::Db& db,
, _defaultCoverPath {defaultCoverPath}
, _maxCacheSize {Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
, _maxFileSize {Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
+ , _preferredFileNames {constructPreferredFileNames()}
{
setJpegQuality(Service::get()->getULong("cover-jpeg-quality", 75));
@@ -101,6 +116,7 @@ CoverService::CoverService(Database::Db& db,
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
+ LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
#if LMS_SUPPORT_IMAGE_GM
GraphicsMagick::init(execPath);
@@ -354,6 +370,7 @@ CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId track
std::shared_ptr
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
{
+ using namespace Database;
const CacheEntryDesc cacheEntryDesc {releaseId, width};
std::shared_ptr cover {loadFromCache(cacheEntryDesc)};
@@ -362,11 +379,11 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
struct ReleaseInfo
{
- Database::TrackId firstTrackId;
+ TrackId firstTrackId;
std::filesystem::path releaseDirectory;
};
- Database::Session& session {_db.getTLSSession()};
+ Session& session {_db.getTLSSession()};
auto getReleaseInfo {[&]
{
@@ -374,13 +391,15 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
auto transaction {session.createSharedTransaction()};
- if (const Database::Release::pointer release {Database::Release::find(session, releaseId)})
+ const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))};
+
+ if (!tracks.results.empty())
{
- if (const auto firstTrack {release->getFirstTrack()})
+ if (const Track::pointer track {Track::find(session, tracks.results.front())})
{
res = ReleaseInfo {};
- res->firstTrackId = firstTrack->getId();
- res->releaseDirectory = firstTrack->getPath().parent_path();
+ res->firstTrackId = track->getId();
+ res->releaseDirectory = track->getPath().parent_path();
}
}
diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp
index bf6e9d6c..f23d437a 100644
--- a/src/libs/services/cover/impl/CoverService.hpp
+++ b/src/libs/services/cover/impl/CoverService.hpp
@@ -129,7 +129,7 @@ namespace Cover
const std::size_t _maxCacheSize;
static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
const std::size_t _maxFileSize;
- static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize
+ const std::vector _preferredFileNames;
unsigned _jpegQuality;
};
diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp
index 575cb675..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 ") + ")");
@@ -136,7 +131,8 @@ createQuery(Session& session, const Artist::FindParameters& params)
assert(params.scrobbler);
query.join("starred_artist s_a ON s_a.artist_id = a.id")
.where("s_a.user_id = ?").bind(params.starringUser)
- .where("s_a.scrobbler = ?").bind(*params.scrobbler);
+ .where("s_a.scrobbler = ?").bind(*params.scrobbler)
+ .where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
if (!params.clusters.empty())
@@ -192,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
@@ -201,115 +197,7 @@ Artist::find(Session& session, const FindParameters& params)
session.checkSharedLocked();
auto query {createQuery(session, params)};
- return execQuery(query, params.range);
-}
-
-RangeResults
-Artist::getReleases(Range range, const std::vector& clusterIds) const
-{
- assert(session());
-
- WhereClause where;
-
- std::ostringstream oss;
-
- oss << "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";
-
- if (!clusterIds.empty())
- {
- oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
-
- WhereClause clusterClause;
-
- for (auto id : clusterIds)
- clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
-
- where.And(clusterClause);
- }
-
- where.And(WhereClause("a.id = ?")).bind(getId().toString());
-
- oss << " " << where.get();
-
- if (!clusterIds.empty())
- oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
-
- oss << " ORDER BY t.date DESC, r.name COLLATE NOCASE";
-
- auto query {session()->query(oss.str())};
-
- for (const std::string& bindArg : where.getBindArgs())
- query.bind(bindArg);
-
- return execQuery(query, range);
-}
-
-std::size_t
-Artist::getReleaseCount() const
-{
- assert(session());
-
- int res = session()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id INNER JOIN track t ON t.release_id = r.id")
- .where("a.id = ?").bind(getId());
- return res;
-}
-
-std::vector
-Artist::getTracks(std::optional linkType) const
-{
- assert(session());
-
- auto query {session()->query>("SELECT DISTINCT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
- .where("a.id = ?").bind(getId())
- .orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number")};
-
- if (linkType)
- query.where("t_a_l.type = ?").bind(*linkType);
-
- auto tracks {query.resultList()};
- return std::vector(tracks.begin(), tracks.end());
-}
-
-RangeResults
-Artist::getNonReleaseTracks(std::optional linkType, Range range) const
-{
- assert(session());
-
- auto query {session()->query>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
- .where("a.id = ?").bind(getId())
- .where("t.release_id is NULL")
- .orderBy("t.name")};
- if (linkType)
- query.where("t_a_l.type = ?").bind(*linkType);
-
- return execQuery(query, range);
-}
-
-bool
-Artist::hasNonReleaseTracks(std::optional linkType) const
-{
- auto query {session()->query>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
- .where("a.id = ?").bind(getId())
- .where("t.release_id is NULL")
- .orderBy("t.name")};
-
- if (linkType)
- query.where("t_a_l.type = ?").bind(*linkType);
-
- return !query.resultList().empty();
-}
-
-std::vector
-Artist::getRandomTracks(std::optional count) const
-{
- assert(session());
-
- Wt::Dbo::collection> tracks {session()->query>("SELECT t from track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
- .where("a.id = ?").bind(getId())
- .orderBy("RANDOM()")
- .limit(count ? static_cast(*count) : -1)};
-
- return std::vector(tracks.begin(), tracks.end());
+ return Utils::execQuery(query, params.range);
}
RangeResults
@@ -353,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 b83ebee2..ef897499 100644
--- a/src/libs/services/database/impl/Migration.cpp
+++ b/src/libs/services/database/impl/Migration.cpp
@@ -549,6 +549,48 @@ CREATE TABLE "listen" (
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
}
+ static
+ void
+ migrateFromV33(Session& session)
+ {
+ // remove name from track_artist_link
+ // Drop Auth mode
+ session.getDboSession().execute(R"(
+CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "type" integer not null,
+ "track_id" bigint,
+ "artist_id" bigint,
+ constraint "fk_track_artist_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
+);
+))");
+ session.getDboSession().execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link");
+ session.getDboSession().execute("DROP TABLE track_artist_link");
+ session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
+ }
+
+ static
+ void
+ migrateFromV34(Session& session)
+ {
+ // Add scrobbling state
+ // 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)
{
@@ -588,6 +630,9 @@ CREATE TABLE "listen" (
{30, migrateFromV30},
{31, migrateFromV31},
{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 9b4f1eed..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 {33};
+ 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 c268ef7c..1b2bf630 100644
--- a/src/libs/services/database/impl/Release.cpp
+++ b/src/libs/services/database/impl/Release.cpp
@@ -41,7 +41,8 @@ createQuery(Session& session, const Release::FindParameters& params)
if (params.sortMethod == ReleaseSortMethod::LastWritten
|| params.writtenAfter.isValid()
- || params.dateRange)
+ || params.dateRange
+ || params.artist.isValid())
{
query.join("track t ON t.release_id = r.id");
}
@@ -56,14 +57,64 @@ 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())
{
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.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())
@@ -103,6 +154,9 @@ createQuery(Session& session, const Release::FindParameters& params)
case ReleaseSortMethod::Date:
query.orderBy("t.date, r.name COLLATE NOCASE");
break;
+ case ReleaseSortMethod::DateDesc:
+ query.orderBy("t.date DESC, r.name COLLATE NOCASE");
+ break;
case ReleaseSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
query.orderBy("s_r.date_time DESC");
@@ -118,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)
{
@@ -160,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)
{
@@ -184,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"
@@ -192,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
@@ -201,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
@@ -211,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
@@ -352,64 +401,12 @@ Release::hasVariousArtists() const
return getArtists().size() > 1;
}
-std::vector
-Release::getTracks(const std::vector& clusterIds) const
-{
- assert(session());
-
- WhereClause where;
-
- std::ostringstream oss;
- oss << "SELECT t FROM track t INNER JOIN release r ON t.release_id = r.id";
-
- if (!clusterIds.empty())
- {
- oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
-
- WhereClause clusterClause;
-
- for (auto id : clusterIds)
- clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
-
- where.And(clusterClause);
- }
-
- where.And(WhereClause("r.id = ?")).bind(getId().toString());
-
- oss << " " << where.get();
-
- if (!clusterIds.empty())
- oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
-
- oss << " ORDER BY t.disc_number,t.track_number";
-
- auto query {session()->query>(oss.str())};
- for (const std::string& bindArg : where.getBindArgs())
- query.bind(bindArg);
-
- auto res {query.resultList()};
- return std::vector (res.begin(), res.end());
-}
-
std::size_t
Release::getTracksCount() const
{
return _tracks.size();
}
-Track::pointer
-Release::getFirstTrack() const
-{
- assert(session());
-
- return session()->query>("SELECT t from track t")
- .join("release r ON t.release_id = r.id")
- .where("r.id = ?").bind(getId())
- .orderBy("t.disc_number,t.track_number")
- .limit(1)
- .resultValue();
-}
-
std::chrono::milliseconds
Release::getDuration() const
{
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/Session.cpp b/src/libs/services/database/impl/Session.cpp
index 660e7082..ec41e43c 100644
--- a/src/libs/services/database/impl/Session.cpp
+++ b/src/libs/services/database/impl/Session.cpp
@@ -151,9 +151,9 @@ Session::prepareTables()
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
- _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
+ _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)");
diff --git a/src/libs/services/database/impl/StarredArtist.cpp b/src/libs/services/database/impl/StarredArtist.cpp
index ed617665..ca4b3638 100644
--- a/src/libs/services/database/impl/StarredArtist.cpp
+++ b/src/libs/services/database/impl/StarredArtist.cpp
@@ -22,6 +22,7 @@
#include
#include "services/database/Artist.hpp"
+#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
@@ -35,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)
{
@@ -60,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 c72331d5..17f1c598 100644
--- a/src/libs/services/database/impl/StarredRelease.cpp
+++ b/src/libs/services/database/impl/StarredRelease.cpp
@@ -22,6 +22,7 @@
#include
#include "services/database/Release.hpp"
+#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
@@ -35,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)
{
@@ -60,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 6543654f..21158c9a 100644
--- a/src/libs/services/database/impl/StarredTrack.cpp
+++ b/src/libs/services/database/impl/StarredTrack.cpp
@@ -22,6 +22,7 @@
#include
#include "services/database/Track.hpp"
+#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
@@ -35,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)
{
@@ -60,20 +67,26 @@ namespace Database
.resultValue();
}
- StarredTrack::pointer
- StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler)
+ RangeResults
+ StarredTrack::find(Session& session, const FindParameters& params)
{
- session.checkUniqueLocked();
+ session.checkSharedLocked();
- StarredTrack::pointer res {session.getDboSession().add(std::make_unique(track, user, scrobbler))};
- session.getDboSession().flush();
+ auto query {session.getDboSession().query("SELECT DISTINCT s_t.id FROM starred_track s_t")};
- return res;
+ if (params.scrobbler)
+ query.where("s_t.scrobbler = ?").bind(*params.scrobbler);
+ if (params.scrobblingState)
+ query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState);
+ if (params.user.isValid())
+ query.where("s_t.user_id = ?").bind(params.user);
+
+ 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 6c1c2a94..269ba3a9 100644
--- a/src/libs/services/database/impl/Track.cpp
+++ b/src/libs/services/database/impl/Track.cpp
@@ -43,10 +43,10 @@ createQuery(Session& session, const Track::FindParameters& params)
{
session.checkSharedLocked();
- auto query {session.getDboSession().query("SELECT t.id from track t")};
+ auto query {session.getDboSession().query(params.distinct ? "SELECT DISTINCT t.id FROM track t" : "SELECT 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);
@@ -56,7 +56,8 @@ createQuery(Session& session, const Track::FindParameters& params)
assert(params.scrobbler);
query.join("starred_track s_t ON s_t.track_id = t.id")
.where("s_t.user_id = ?").bind(params.starringUser)
- .where("s_t.scrobbler = ?").bind(*params.scrobbler);
+ .where("s_t.scrobbler = ?").bind(*params.scrobbler)
+ .where("s_t.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
}
if (!params.clusters.empty())
@@ -79,6 +80,43 @@ createQuery(Session& session, const Track::FindParameters& params)
query.where(oss.str());
}
+ 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());
+ }
+ }
+
+ assert(!(params.nonRelease && params.release.isValid()));
+ if (params.nonRelease)
+ query.where("t.release_id IS NULL");
+ else if (params.release.isValid())
+ query.where("t.release_id = ?").bind(params.release);
+
+ if (params.trackList.isValid())
+ {
+ query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
+ query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id");
+ query.where("t_l.id = ?").bind(params.trackList);
+ }
+
switch (params.sortMethod)
{
case TrackSortMethod::None:
@@ -93,6 +131,18 @@ createQuery(Session& session, const Track::FindParameters& params)
assert(params.starringUser.isValid());
query.orderBy("s_t.date_time DESC");
break;
+ case TrackSortMethod::Name:
+ query.orderBy("t.name COLLATE NOCASE");
+ break;
+ case TrackSortMethod::DateDescAndRelease:
+ query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number");
+ break;
+ case TrackSortMethod::Release:
+ query.orderBy("t.disc_number,t.track_number");
+ break;
+ case TrackSortMethod::TrackList:
+ assert(params.trackList.isValid());
+ query.orderBy("t_l.id");
}
return query;
@@ -106,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
@@ -169,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;
@@ -193,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
@@ -205,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
@@ -234,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
@@ -247,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
@@ -278,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 58ea58b2..a212f13c 100644
--- a/src/libs/services/database/impl/TrackList.cpp
+++ b/src/libs/services/database/impl/TrackList.cpp
@@ -35,24 +35,21 @@
namespace Database {
-TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr user)
-: _name {name},
- _type {type},
- _isPublic {isPublic},
- _user {getDboPtr(user)}
+TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user)
+ : _name {name}
+, _type {type}
+, _isPublic {isPublic}
+, _creationDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
+, _lastModifiedDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
+, _user {getDboPtr(user)}
{
+ assert(user);
}
TrackList::pointer
-TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr user)
+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
@@ -65,7 +62,7 @@ TrackList::getCount(Session& session)
TrackList::pointer
-TrackList::find(Session& session, std::string_view name, Type type, UserId userId)
+TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
{
session.checkSharedLocked();
assert(userId.isValid());
@@ -77,28 +74,54 @@ TrackList::find(Session& session, std::string_view name, Type type, UserId userI
}
RangeResults
-TrackList::find(Session& session, UserId userId, Range range)
+TrackList::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
- auto query {session.getDboSession().query("SELECT id FROM tracklist")
- .where("user_id = ?").bind(userId)
- .orderBy("name COLLATE NOCASE")};
+ auto query {session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l")};
- return execQuery(query, range);
-}
+ if (params.user.isValid())
+ query.where("t_l.user_id = ?").bind(params.user);
-RangeResults
-TrackList::find(Session& session, UserId userId, Type type, Range range)
-{
- session.checkSharedLocked();
+ if (params.type)
+ query.where("t_l.type = ?").bind(*params.type);
- auto query {session.getDboSession().query("SELECT id FROM tracklist")
- .where("user_id = ?").bind(userId)
- .where("type = ?").bind(type)
- .orderBy("name COLLATE NOCASE")};
+ if (!params.clusters.empty())
+ {
+ query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
+ query.join("track t ON t.id = t_l_e.track_id");
- return execQuery(query, range);
+ std::ostringstream oss;
+ oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
+ " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
+ " INNER JOIN cluster c ON c.id = t_c.cluster_id";
+
+ WhereClause clusterClause;
+ for (const ClusterId clusterId : 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());
+ }
+
+ 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 Utils::execQuery(query, params.range);
}
TrackList::pointer
@@ -157,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();
}
@@ -443,6 +466,57 @@ TrackList::getClusters() const
return std::vector(res.begin(), res.end());
}
+std::vector>
+TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const
+{
+ assert(session());
+ std::vector> res;
+
+ if (clusterTypes.empty())
+ return res;
+
+ auto query {session()->query>("SELECT c from cluster c")};
+
+ query.join("track t ON c.id = t_c.cluster_id")
+ .join("track_cluster t_c ON t_c.track_id = t.id")
+ .join("cluster_type c_type ON c.cluster_type_id = c_type.id")
+ .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id")
+ .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id")
+ .where("t_l.id = ?").bind(getId());
+
+ {
+ std::ostringstream oss;
+ oss << "c_type.id IN (";
+ bool first {true};
+ for (auto clusterType : clusterTypes)
+ {
+ if (!first)
+ oss << ", ";
+ oss << "?";
+ query.bind(clusterType ->getId());
+ first = false;
+ }
+ oss << ")";
+ query.where(oss.str());
+ }
+ query.groupBy("c.id");
+ query.orderBy("COUNT(c.id) DESC");
+
+ 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);
+ }
+
+ for (const auto& [clusterTypeId, clusters] : clustersByType)
+ res.push_back(clusters);
+
+ return res;
+}
+
bool
TrackList::hasTrack(TrackId trackId) const
{
@@ -502,6 +576,12 @@ TrackList::getDuration() const
return query.resultValue();
}
+void
+TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
+{
+ _lastModifiedDateTime = Utils::normalizeDateTime(dateTime);
+}
+
std::vector
TrackList::getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const
{
@@ -580,23 +660,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(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
+}
- return res;
+void
+TrackListEntry::onPreRemove()
+{
+ _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(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 8fd4b1a9..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);
@@ -89,13 +88,6 @@ class Artist : public Object
const std::string& getSortName() const { return _sortName; }
std::optional getMBID() const { return UUID::fromString(_MBID); }
- RangeResults getReleases(Range range, const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
- std::size_t getReleaseCount() const;
- std::vector> getTracks(std::optional linkType = {}) const;
- bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const;
- RangeResults> getNonReleaseTracks(std::optional linkType, Range range) const;
- std::vector> getRandomTracks(std::optional count) const;
-
// No artistLinkTypes means get them all
RangeResults findSimilarArtists(EnumSet artistLinkTypes = {}, Range range = {}) const;
@@ -108,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)
{
@@ -125,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..1287349e 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,12 @@ namespace Database
bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; }
auto modify() { return _obj.modify(); }
- void remove() { _obj.remove(); }
+ void remove()
+ {
+ if (_obj->hasOnPreRemove())
+ _obj.modify()->onPreRemove();
+ _obj.remove();
+ }
private:
template friend class Object;
@@ -61,6 +67,14 @@ namespace Database
typename Wt::Dbo::dbo_traits::IdType id() const = delete;
protected:
+ template friend class ObjectPtr;
+
+ virtual bool hasOnPreRemove() const { return false; }
+ virtual void onPreRemove() {}
+
+ virtual bool hasOnPostCreated() const { return false; }
+ virtual void onPostCreated() {}
+
// 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 999899a6..0bf62e43 100644
--- a/src/libs/services/database/include/services/database/Release.hpp
+++ b/src/libs/services/database/include/services/database/Release.hpp
@@ -25,11 +25,13 @@
#include
#include
+#include "services/database/ArtistId.hpp"
#include "services/database/ClusterId.hpp"
#include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/Types.hpp"
#include "services/database/UserId.hpp"
+#include "utils/EnumSet.hpp"
#include "utils/UUID.hpp"
namespace Database
@@ -54,8 +56,11 @@ class Release : public Object
Range range;
Wt::WDateTime writtenAfter;
std::optional dateRange;
- UserId starringUser; // only releases starred by this user
- std::optional scrobbler; // and for this scrobbler
+ 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
FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; }
@@ -64,10 +69,16 @@ class Release : public Object
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