Merge branch 'develop' for release v3.30.0
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
<div class="p-2 flex-fill">
|
||||
${name}${<if-tag>}<span class="badge bg-info ms-2">${tag}</span>${</if-tag>}
|
||||
</div>
|
||||
${<if-edit>}
|
||||
<div class="p-2">
|
||||
${<if-edit>}${edit-btn class="btn btn-sm btn-secondary me-1"}${del-btn class="btn btn-sm btn-danger"}${</if-edit>}
|
||||
${edit-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
${del-btn class="btn btn-sm btn-outline-danger border-0"}
|
||||
</div>
|
||||
${</if-edit>}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
@@ -25,12 +28,9 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Admin.Users.del-user}</h5>
|
||||
<h5 class="modal-title">${tr:Lms.Admin.Users.del-user-confirm}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${tr:Lms.Admin.Users.del-user-name}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${del-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
|
||||
+17
-13
@@ -4,33 +4,37 @@
|
||||
|
||||
<message id="Lms.Explore.Artist.template">
|
||||
<div class="mb-3">
|
||||
<h3>${name}</h3>
|
||||
<h2>${name}</h2>
|
||||
${clusters class="mb-2"}
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
|
||||
${</if-has-mbid>}
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-release>}
|
||||
${<if-has-releases>}
|
||||
${<if-section-titles>}<h3>${tr:Lms.Explore.releases}</h3>${</if-section-titles>}
|
||||
${releases class="mb-3"}
|
||||
${</if-has-release>}
|
||||
${<if-has-non-release-track>}
|
||||
${</if-has-releases>}
|
||||
${<if-has-appears-on-releases>}
|
||||
${<if-section-titles>}<h3>${tr:Lms.Explore.Artist.appears-on}</h3>${</if-section-titles>}
|
||||
${appears-on-releases class="mb-3"}
|
||||
${</if-has-appears-on-releases>}
|
||||
${<if-has-non-release-tracks>}
|
||||
${<if-section-titles>}<h3>${tr:Lms.Explore.tracks}</h3>${</if-section-titles>}
|
||||
${tracks class="mb-3"}
|
||||
${</if-has-non-release-track>}
|
||||
${</if-has-non-release-tracks>}
|
||||
${<if-has-similar-artists>}
|
||||
<div>
|
||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||
${similar-artists class="d-grid gap-3"}
|
||||
</div>
|
||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||
${similar-artists class="d-grid gap-3"}
|
||||
${</if-has-similar-artists>}
|
||||
</message>
|
||||
|
||||
|
||||
+3
-6
@@ -5,9 +5,6 @@
|
||||
${contents}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
|
||||
<message id="Lms.Explore.template.filters">
|
||||
${add-filter class="btn btn-sm btn-outline-primary me-2"}${clusters class="d-inline-block align-items-center"}
|
||||
</message>
|
||||
@@ -35,9 +32,9 @@
|
||||
${value class="form-control"}
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
${add-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${add-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -14,9 +14,9 @@
|
||||
<message id="Lms.main.template.navbar">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-3">
|
||||
<div class="container">
|
||||
<form class="d-flex">
|
||||
<div class="d-flex">
|
||||
${search class="form-control" type="search"}
|
||||
</form>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@@ -32,7 +32,7 @@
|
||||
${tracks class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${playqueue class="nav-link"}
|
||||
${tracklists class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${filters class="nav-link d-flex align-items-center"}
|
||||
@@ -44,7 +44,7 @@
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="${tr:Lms.administration}">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li>${database class="dropdown-item"}</li>
|
||||
<li>${users class="dropdown-item"}</li>
|
||||
</ul>
|
||||
@@ -54,7 +54,7 @@
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="${tr:Lms.user}">
|
||||
<i class="fa fa-user" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
<li class="dropdown-divider"></li>
|
||||
<li>${settings class="dropdown-item"}</li>
|
||||
|
||||
+10
-5
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-inline-flex me-2">
|
||||
<div class="py-2 pe-2 d-inline-flex">
|
||||
<button id="lms-mp-previous" class="btn btn-sm btn-outline-primary border-0">
|
||||
<i class="fa fa-fw fa-step-backward"/>
|
||||
</button>
|
||||
@@ -29,16 +29,21 @@
|
||||
${title class="d-block text-truncate"}
|
||||
${release class="text-decoration-none text-truncate link-success"}
|
||||
</div>
|
||||
<div class="d-none d-md-flex align-items-center Lms-player-volume-container">
|
||||
<div class="p-2 d-none d-sm-block text-muted text-nowrap">
|
||||
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
|
||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||
</div>
|
||||
<div class="p-2 d-none d-md-flex align-items-center Lms-player-volume-container">
|
||||
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up link-primary me-1"></i>
|
||||
<input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8"/>
|
||||
</div>
|
||||
<div class="d-none d-sm-block text-muted text-nowrap">
|
||||
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
|
||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||
<div class="py-2 ps-2">
|
||||
${playqueue-btn class="btn btn-outline-primary border-0"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.MediaPlayer.template.playqueue-btn"><i class="fa fa-fw fa-th-list"/></message>
|
||||
|
||||
</messages>
|
||||
|
||||
+17
-6
@@ -6,9 +6,11 @@
|
||||
<message id="Lms.add">Add</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Apply</message>
|
||||
<message id="Lms.discard">Discard</message>
|
||||
<message id="Lms.cancel">Cancel</message>
|
||||
<message id="Lms.create">Create</message>
|
||||
<message id="Lms.delete">Delete</message>
|
||||
<message id="Lms.discard">Discard</message>
|
||||
<message id="Lms.edit">Edit</message>
|
||||
<message id="Lms.loading">Loading...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
@@ -24,6 +26,10 @@
|
||||
<message id="Lms.passwords-dont-match">Passwords don't match</message>
|
||||
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
|
||||
<message id="Lms.save">Save</message>
|
||||
<message id="Lms.track-count">
|
||||
<plural case="0">{1} track</plural>
|
||||
<plural case="1">{1} tracks</plural>
|
||||
</message>
|
||||
<message id="Lms.user">User</message>
|
||||
|
||||
<!--Errors-->
|
||||
@@ -31,6 +37,7 @@
|
||||
<message id="Lms.Error.error-occurred">Error occured!</message>
|
||||
<message id="Lms.Error.go-home">Go home</message>
|
||||
<message id="Lms.Error.release-not-found">Release not found</message>
|
||||
<message id="Lms.Error.tracklist-not-found">Playlist not found</message>
|
||||
<message id="Lms.Error.user-not-allowed">You are not allowed to perform this operation</message>
|
||||
<message id="Lms.Error.user-not-found">User not found</message>
|
||||
|
||||
@@ -85,11 +92,8 @@
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">New user</message>
|
||||
<message id="Lms.Admin.Users.admin">Admin</message>
|
||||
<message id="Lms.Admin.Users.del">Delete</message>
|
||||
<message id="Lms.Admin.Users.del-user">Delete user</message>
|
||||
<message id="Lms.Admin.Users.del-user-name">Delete user?</message>
|
||||
<message id="Lms.Admin.Users.del-user-confirm">Delete user?</message>
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.edit">Edit</message>
|
||||
<message id="Lms.Admin.Users.users">Users</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||
|
||||
@@ -126,10 +130,12 @@
|
||||
<message id="Lms.Explore.play-shuffled">Play shuffled</message>
|
||||
<message id="Lms.Explore.random">Random</message>
|
||||
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.star">Star</message>
|
||||
<message id="Lms.Explore.starred">Starred</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Tracks</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
<message id="Lms.Explore.unstar">Unstar</message>
|
||||
@@ -137,6 +143,7 @@
|
||||
<message id="Lms.Explore.various-artists">Various artists</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.Artist.appears-on">Appears on</message>
|
||||
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
@@ -154,6 +161,9 @@
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
||||
|
||||
<!--Explore:TrackLists-->
|
||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Delete playlist?</message>
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||
|
||||
@@ -162,7 +172,7 @@
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
||||
<message id="Lms.PlayQueue.create-tracklist">Create new playlist</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">
|
||||
<plural case="0">Added {1} track</plural>
|
||||
<plural case="1">Added {1} tracks</plural>
|
||||
@@ -171,6 +181,7 @@
|
||||
<message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">Radio mode</message>
|
||||
<message id="Lms.PlayQueue.repeat">Repeat</message>
|
||||
<message id="Lms.PlayQueue.replace-tracklist">Replace existing playlist</message>
|
||||
<message id="Lms.PlayQueue.shuffle">Shuffle</message>
|
||||
|
||||
<!--PlayHistory-->
|
||||
|
||||
+17
-7
@@ -6,9 +6,11 @@
|
||||
<message id="Lms.add">Ajouter</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Appliquer</message>
|
||||
<message id="Lms.discard">Annuler</message>
|
||||
<message id="Lms.cancel">Annuler</message>
|
||||
<message id="Lms.create">Créer</message>
|
||||
<message id="Lms.delete">Supprimer</message>
|
||||
<message id="Lms.discard">Annuler</message>
|
||||
<message id="Lms.edit">Editer</message>
|
||||
<message id="Lms.loading">Chargement...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
|
||||
@@ -24,6 +26,10 @@
|
||||
<message id="Lms.passwords-dont-match">Les mots de passe ne correspondent pas</message>
|
||||
<message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
|
||||
<message id="Lms.save">Sauvegarder</message>
|
||||
<message id="Lms.track-count">
|
||||
<plural case="0">{1} piste</plural>
|
||||
<plural case="1">{1} pistes</plural>
|
||||
</message>
|
||||
<message id="Lms.user">Utilisateur</message>
|
||||
|
||||
<!--Errors-->
|
||||
@@ -31,6 +37,7 @@
|
||||
<message id="Lms.Error.error-occurred">Une erreur est survenue!</message>
|
||||
<message id="Lms.Error.go-home">Retour à l'accueil</message>
|
||||
<message id="Lms.Error.release-not-found">Cet album n'existe pas</message>
|
||||
<message id="Lms.Error.tracklist-not-found">Cette liste de lecture n'existe pas</message>
|
||||
<message id="Lms.Error.user-not-allowed">Vous n'avez pas les droits pour effectuer cette opération</message>
|
||||
<message id="Lms.Error.user-not-found">L'utilisateur n'existe pas</message>
|
||||
|
||||
@@ -85,11 +92,8 @@
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">Ajouter</message>
|
||||
<message id="Lms.Admin.Users.admin">Admin</message>
|
||||
<message id="Lms.Admin.Users.del">Supprimer</message>
|
||||
<message id="Lms.Admin.Users.del-user">Supprimer l'utilisateur</message>
|
||||
<message id="Lms.Admin.Users.del-user-name">Supprimer l'utilisateur ?</message>
|
||||
<message id="Lms.Admin.Users.del-user-confirm">Supprimer l'utilisateur ?</message>
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.edit">Editer</message>
|
||||
<message id="Lms.Admin.Users.users">Utilisateurs</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||
|
||||
@@ -126,10 +130,12 @@
|
||||
<message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message>
|
||||
<message id="Lms.Explore.random">Aléatoire</message>
|
||||
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
|
||||
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
||||
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||
<message id="Lms.Explore.starred">Favoris</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Pistes</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
<message id="Lms.Explore.unstar">Retirer des favoris</message>
|
||||
@@ -137,6 +143,7 @@
|
||||
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.Artist.appears-on">Apparaît dans</message>
|
||||
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
@@ -154,6 +161,9 @@
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
||||
|
||||
<!--Explore:TrackLists-->
|
||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">Supprimer la liste de lecture ?</message>
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||
|
||||
@@ -162,7 +172,7 @@
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Effacer</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
|
||||
<message id="Lms.PlayQueue.create-tracklist">Créer une nouvelle liste de lecture</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">
|
||||
<plural case="0">{1} piste ajoutée</plural>
|
||||
<plural case="1">{1} pistes ajoutées</plural>
|
||||
@@ -171,12 +181,12 @@
|
||||
<message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">Mode radio</message>
|
||||
<message id="Lms.PlayQueue.repeat">Repéter</message>
|
||||
<message id="Lms.PlayQueue.replace-tracklist">Remplacer une liste de lecture existante</message>
|
||||
<message id="Lms.PlayQueue.shuffle">Mélanger</message>
|
||||
|
||||
<!--PlayHistory-->
|
||||
<message id="Lms.PlayHistory.playhistory">Historique de lecture</message>
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
||||
|
||||
+23
-7
@@ -6,9 +6,11 @@
|
||||
<message id="Lms.add">Aggiungi</message>
|
||||
<message id="Lms.administration">Amministrazione</message>
|
||||
<message id="Lms.apply">Applica</message>
|
||||
<message id="Lms.discard">Annulla</message>
|
||||
<message id="Lms.cancel">Annulla</message>
|
||||
<message id="Lms.create">Crea</message>
|
||||
<message id="Lms.del">Elimina</message>
|
||||
<message id="Lms.discard">Annulla</message>
|
||||
<message id="Lms.edit">Modifica</message>
|
||||
<message id="Lms.loading">Caricamento...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
@@ -24,6 +26,10 @@
|
||||
<message id="Lms.passwords-dont-match">Le password non corrispondono</message>
|
||||
<message id="Lms.quit-other-session">Un'altra sessione è stata aperta. Vuoi riaprire questa?</message>
|
||||
<message id="Lms.save">Salva</message>
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.user">Utente</message>
|
||||
|
||||
<!--Errors-->
|
||||
@@ -31,6 +37,7 @@
|
||||
<message id="Lms.Error.error-occurred">C'è stato un errore!</message>
|
||||
<message id="Lms.Error.go-home">Ritorna alla home</message>
|
||||
<message id="Lms.Error.release-not-found">Album non trovato</message>
|
||||
|
||||
<message id="Lms.Error.user-not-allowed">Non sei autorizzato ad eseguire questa operazione</message>
|
||||
<message id="Lms.Error.user-not-found">Utente non trovato</message>
|
||||
|
||||
@@ -43,6 +50,7 @@
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Raccolta musicale</message>
|
||||
<message id="Lms.Admin.Database.never">Mai</message>
|
||||
<message id="Lms.Admin.Database.path">Cartella dei file multimediali</message>
|
||||
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">Modalità di raccomandazione brani consigliati</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Basata sui tag</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.features">Basata sull'analisi acustica</message>
|
||||
@@ -84,11 +92,8 @@
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">Nuovo utente</message>
|
||||
<message id="Lms.Admin.Users.admin">Amministratore</message>
|
||||
<message id="Lms.Admin.Users.del">Elimina</message>
|
||||
<message id="Lms.Admin.Users.del-user">Elimina l'utente</message>
|
||||
<message id="Lms.Admin.Users.del-user-name">Vuoi eliminare l'utente?</message>
|
||||
<message id="Lms.Admin.Users.del-user-confirm">Elimina l'utente ?</message>
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.edit">Modifica</message>
|
||||
<message id="Lms.Admin.Users.users">Utenti</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message>
|
||||
|
||||
@@ -125,10 +130,12 @@
|
||||
<message id="Lms.Explore.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message>
|
||||
<message id="Lms.Explore.random">Casuali</message>
|
||||
<message id="Lms.Explore.recently-added">Aggiunti di recente</message>
|
||||
|
||||
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
|
||||
<message id="Lms.Explore.releases">Album</message>
|
||||
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
||||
<message id="Lms.Explore.starred">Preferiti</message>
|
||||
|
||||
<message id="Lms.Explore.tracks">Tracce</message>
|
||||
<message id="Lms.Explore.type">Tipo</message>
|
||||
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
|
||||
@@ -136,6 +143,7 @@
|
||||
<message id="Lms.Explore.various-artists">Vari artisti</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
|
||||
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
@@ -153,6 +161,9 @@
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
||||
|
||||
<!--Explore:TrackLists-->
|
||||
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
|
||||
|
||||
@@ -161,7 +172,7 @@
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Cancella</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} tracce</message>
|
||||
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">
|
||||
<plural case="0">Aggiunta {1} traccia</plural>
|
||||
<plural case="1">Aggiunte {1} tracce</plural>
|
||||
@@ -170,12 +181,12 @@
|
||||
<message id="Lms.PlayQueue.playqueue-full">Coda di riproduzione piena!</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">Modalità radio</message>
|
||||
<message id="Lms.PlayQueue.repeat">Ripeti</message>
|
||||
|
||||
<message id="Lms.PlayQueue.shuffle">Mischia</message>
|
||||
|
||||
<!--PlayHistory-->
|
||||
<message id="Lms.PlayHistory.playhistory">Storico riproduzione</message>
|
||||
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
||||
@@ -191,6 +202,11 @@
|
||||
<message id="Lms.Settings.replaygain-mode.release">Album</message>
|
||||
<message id="Lms.Settings.replaygain-preamp">Preamplificazione ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Preamplificazione ReplayGain (se non è disponibile nessuna informazione)</message>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||
|
||||
+22
-6
@@ -6,9 +6,11 @@
|
||||
<message id="Lms.add">添加</message>
|
||||
<message id="Lms.administration">管理</message>
|
||||
<message id="Lms.apply">应用</message>
|
||||
<message id="Lms.discard">撤销</message>
|
||||
<message id="Lms.cancel">取消</message>
|
||||
<message id="Lms.create">新建</message>
|
||||
<message id="Lms.del">删除</message>
|
||||
<message id="Lms.discard">撤销</message>
|
||||
<message id="Lms.edit">编辑</message>
|
||||
<message id="Lms.loading">加载中...</message>
|
||||
<message id="Lms.login">登陆</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
|
||||
@@ -24,6 +26,10 @@
|
||||
<message id="Lms.passwords-dont-match">密码不匹配</message>
|
||||
<message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message>
|
||||
<message id="Lms.save">保存</message>
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.user">用户</message>
|
||||
|
||||
<!--Errors-->
|
||||
@@ -31,6 +37,7 @@
|
||||
<message id="Lms.Error.error-occurred">发生错误!</message>
|
||||
<message id="Lms.Error.go-home">返回主页</message>
|
||||
<message id="Lms.Error.release-not-found">发布未找到</message>
|
||||
|
||||
<message id="Lms.Error.user-not-allowed">不允许执行此操作</message>
|
||||
<message id="Lms.Error.user-not-found">未找到此用户</message>
|
||||
|
||||
@@ -85,11 +92,8 @@
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">新建用户</message>
|
||||
<message id="Lms.Admin.Users.admin">管理员</message>
|
||||
<message id="Lms.Admin.Users.del">删除</message>
|
||||
<message id="Lms.Admin.Users.del-user">删除用户</message>
|
||||
<message id="Lms.Admin.Users.del-user-name">确认要删除此用户?</message>
|
||||
|
||||
<message id="Lms.Admin.Users.demo">演示</message>
|
||||
<message id="Lms.Admin.Users.edit">编辑</message>
|
||||
<message id="Lms.Admin.Users.users">用户</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> 用户</message>
|
||||
|
||||
@@ -126,10 +130,12 @@
|
||||
<message id="Lms.Explore.play-shuffled">随机播放</message>
|
||||
<message id="Lms.Explore.random">随机</message>
|
||||
<message id="Lms.Explore.recently-added">最近添加</message>
|
||||
|
||||
<message id="Lms.Explore.recently-played">最近播放</message>
|
||||
<message id="Lms.Explore.releases">专辑</message>
|
||||
<message id="Lms.Explore.star">收藏</message>
|
||||
<message id="Lms.Explore.starred">已收藏</message>
|
||||
|
||||
<message id="Lms.Explore.tracks">音轨</message>
|
||||
<message id="Lms.Explore.type">类型</message>
|
||||
<message id="Lms.Explore.unstar">取消收藏</message>
|
||||
@@ -137,6 +143,7 @@
|
||||
<message id="Lms.Explore.various-artists">群星</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
|
||||
<message id="Lms.Explore.Artist.similar-artists">相似歌手</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
@@ -154,6 +161,9 @@
|
||||
<message id="Lms.Explore.Release.copyright">版权所有</message>
|
||||
<message id="Lms.Explore.Release.disc">唱片 {1}</message>
|
||||
|
||||
<!--Explore:TrackLists-->
|
||||
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">搜索中...</message>
|
||||
|
||||
@@ -162,7 +172,7 @@
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">清除</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks">{1} 音轨</message>
|
||||
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">
|
||||
<plural case="0">添加 {1} 音轨</plural>
|
||||
<plural case="1">已添加 {1} 音轨</plural>
|
||||
@@ -171,6 +181,7 @@
|
||||
<message id="Lms.PlayQueue.playqueue-full">播放队列已满!</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">广播模式</message>
|
||||
<message id="Lms.PlayQueue.repeat">循环播放</message>
|
||||
|
||||
<message id="Lms.PlayQueue.shuffle">随机播放</message>
|
||||
|
||||
<!--PlayHistory-->
|
||||
@@ -191,6 +202,11 @@
|
||||
<message id="Lms.Settings.replaygain-mode.release">专辑</message>
|
||||
<message id="Lms.Settings.replaygain-preamp">ReplayGain 放大器</message>
|
||||
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain 放大器 (如果没有信息)</message>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||
|
||||
+6
-1
@@ -9,9 +9,14 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.infinite-scrolling-container">
|
||||
<message id="Lms.infinite-scrolling-container.template">
|
||||
${elements}
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
<message id="Lms.template.delete-btn"><i class="fa fa-fw fa-times"></i></message>
|
||||
<message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message>
|
||||
<message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
<message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
|
||||
</messages>
|
||||
|
||||
+59
-14
@@ -6,30 +6,30 @@
|
||||
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random"></i></message>
|
||||
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat"></i></message>
|
||||
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss"></i></message>
|
||||
<message id="Lms.PlayQueue.template.save-btn"><i class="fa fa-fw fa-save"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template">
|
||||
<div class="small text-muted mb-2">${track-count} · ${duration}</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.clear}">
|
||||
${clear-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.shuffle}">
|
||||
${shuffle-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="btn-group me-2">
|
||||
${repeat-btn class="btn-check"}
|
||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.repeat}" for="${id:repeat-btn}">${tr:Lms.PlayQueue.template.repeat-btn}</label>
|
||||
${radio-btn class="btn-check"}
|
||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.radio-mode}" for="${id:radio-btn}">${tr:Lms.PlayQueue.template.radio-btn}</label>
|
||||
</div>
|
||||
${nb-tracks}
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.save}">
|
||||
${save-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.shuffle}">
|
||||
${shuffle-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
<div class="d-inline-block me-2" title="${tr:Lms.PlayQueue.clear}">
|
||||
${clear-btn class="btn btn-warning"}
|
||||
</div>
|
||||
</div>
|
||||
${entries}
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times"></i></message>
|
||||
<message id="Lms.PlayQueue.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry">
|
||||
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
|
||||
<div class="p-1 Lms-cover-small">
|
||||
@@ -44,14 +44,14 @@
|
||||
</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-none d-sm-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
@@ -60,4 +60,49 @@
|
||||
${artist}
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.save-as-tracklist">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.save}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-check-inline">
|
||||
${create-tracklist-btn class="form-check-input" name="action-type"}
|
||||
<label class="form-check-label" for="${id:create-tracklist-btn}">${tr:Lms.PlayQueue.create-tracklist}</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
${replace-tracklist-btn class="form-check-input" name="action-type"}
|
||||
<label class="form-check-label" for="${id:replace-tracklist-btn}">${tr:Lms.PlayQueue.replace-tracklist}</label>
|
||||
</div>
|
||||
</div>
|
||||
${contents}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${save-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.save-as-tracklist.create-tracklist">
|
||||
${name class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${name-info}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.save-as-tracklist.replace-tracklist">
|
||||
${name class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${name-info}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+8
-8
@@ -12,7 +12,7 @@
|
||||
${cover}
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>${name}</h3>
|
||||
<h2>${name}</h2>
|
||||
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
|
||||
${<if-has-various-release-artists>}<div class="text-secondary">${tr:Lms.Explore.various-artists}</div>${</if-has-various-release-artists>}
|
||||
<div class="small text-muted">${<if-has-year>}${year}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>} · ${</if-has-year>}${duration}</div>
|
||||
@@ -21,13 +21,13 @@
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
${<if-has-mbid>}
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
|
||||
${</if-has-mbid>}
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,9 +74,9 @@
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.TrackList.template">
|
||||
<div class="mb-3">
|
||||
<h2>${name}</h2>
|
||||
<div class="small text-muted">${track-count} · ${duration}</div>
|
||||
${clusters class="mb-2"}
|
||||
<div class="btn-group">
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${delete class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
${tracks}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TrackList.template.delete-tracklist">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Explore.TrackLists.del-tracklist-confirm}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${del-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template">
|
||||
<div class="mb-3">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
${recently-modified class="nav-link"}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
${all class="nav-link"}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
${tracklists}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template.entry">
|
||||
${name class="text-decoration-none link-secondary"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.TrackLists.template.container">
|
||||
${elements class="d-grid gap-3"}
|
||||
${loading-indicator}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+5
-5
@@ -29,8 +29,8 @@
|
||||
${play-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${play-shuffled class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${play-shuffled class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,9 +54,9 @@
|
||||
<div class="dropdown d-inline-block">
|
||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||
<li>${play-last class="btn dropdown-item"}</li>
|
||||
<li>${star class="btn dropdown-item"}</li>
|
||||
<li>${download class="btn dropdown-item"}</li>
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+9
-2
@@ -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;
|
||||
|
||||
@@ -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<User>(loginName);
|
||||
user.modify()->setType(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Database::AuthToken>(secretHash, expiry, user)};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
||||
|
||||
|
||||
@@ -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<std::string> constructPreferredFileNames()
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
Service<IConfig>::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<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
|
||||
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
|
||||
, _preferredFileNames {constructPreferredFileNames()}
|
||||
|
||||
{
|
||||
setJpegQuality(Service<IConfig>::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<IEncodedImage>
|
||||
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
using namespace Database;
|
||||
const CacheEntryDesc cacheEntryDesc {releaseId, width};
|
||||
|
||||
std::shared_ptr<IEncodedImage> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace Cover
|
||||
const std::size_t _maxCacheSize;
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
const std::size_t _maxFileSize;
|
||||
static inline const std::vector<std::string> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
const std::vector<std::string> _preferredFileNames;
|
||||
unsigned _jpegQuality;
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ _MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Artist> {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<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
Artist::pointer res {session.getDboSession().add(std::make_unique<Artist>(name, MBID))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<ArtistId>
|
||||
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<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
@@ -201,115 +197,7 @@ Artist::find(Session& session, const FindParameters& params)
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery(session, params)};
|
||||
return execQuery(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Artist::getReleases(Range range, const std::vector<ClusterId>& 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<ReleaseId>(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<int>("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<Track::pointer>
|
||||
Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("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<Track::pointer>(tracks.begin(), tracks.end());
|
||||
}
|
||||
|
||||
RangeResults<Track::pointer>
|
||||
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.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<TrackArtistLinkType> linkType) const
|
||||
{
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(getId())
|
||||
.where("t.release_id is NULL")
|
||||
.orderBy("t.name")};
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
return !query.resultList().empty();
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Artist::getRandomTracks(std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {session()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(getId())
|
||||
.orderBy("RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)};
|
||||
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
@@ -353,10 +241,11 @@ Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> 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<std::vector<Cluster::pointer>>
|
||||
|
||||
@@ -38,12 +38,7 @@ namespace Database
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
return session.getDboSession().add(std::unique_ptr<AuthToken> {new AuthToken {value, expiry, user}});
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -36,6 +36,12 @@ Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster {type, name}});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getCount(Session& session)
|
||||
{
|
||||
@@ -44,24 +50,13 @@ Cluster::getCount(Session& session)
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
Cluster::pointer res {session.getDboSession().add(std::make_unique<Cluster>(type, name))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RangeResults<ClusterId>
|
||||
Cluster::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterId>
|
||||
@@ -70,7 +65,7 @@ Cluster::findOrphans(Session& session, Range range)
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
@@ -95,7 +90,7 @@ Cluster::getTracks(Range range) const
|
||||
auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId())};
|
||||
|
||||
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<ClusterType> {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<ClusterTypeId>
|
||||
@@ -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<ClusterTypeId>("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<ClusterType>(name))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
|
||||
@@ -141,6 +141,12 @@ namespace Database
|
||||
, _track {getDboPtr(track)}
|
||||
{}
|
||||
|
||||
Listen::pointer
|
||||
Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Listen> {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> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
Listen::pointer res {session.getDboSession().add(std::make_unique<Listen>(user, track, scrobbler, dateTime))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Listen::getTopArtists(Session& session,
|
||||
UserId userId,
|
||||
@@ -213,7 +208,7 @@ namespace Database
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
@@ -227,7 +222,7 @@ namespace Database
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId>
|
||||
@@ -241,7 +236,7 @@ namespace Database
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
@@ -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<ReleaseId>
|
||||
@@ -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<TrackId>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||
session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||
session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<UUID>& MBID)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Release> {new Release {name, MBID}});
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::find(Session& session, const std::string& name)
|
||||
{
|
||||
@@ -160,17 +220,6 @@ Release::exists(Session& session, ReleaseId id)
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Release::pointer res {session.getDboSession().add(std::make_unique<Release>(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<ReleaseId>(
|
||||
"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<ReleaseId>
|
||||
@@ -201,7 +250,7 @@ Release::findOrphans(Session& session, Range range)
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")};
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
@@ -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<std::size_t>
|
||||
@@ -352,64 +401,12 @@ Release::hasVariousArtists() const
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Release::getTracks(const std::vector<ClusterId>& 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<Wt::Dbo::ptr<Track>>(oss.str())};
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto res {query.resultList()};
|
||||
return std::vector<Track::pointer> (res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getTracksCount() const
|
||||
{
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Release::getFirstTrack() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
return session()->query<Wt::Dbo::ptr<Track>>("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
|
||||
{
|
||||
|
||||
@@ -120,7 +120,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clu
|
||||
if (!clusterType)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
||||
clusterType = ClusterType::create(session, clusterTypeName);
|
||||
clusterType = session.create<ClusterType>(clusterTypeName);
|
||||
_clusterTypes.insert(getDboPtr(clusterType));
|
||||
|
||||
needRescan = true;
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#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> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredArtist> {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> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
StarredArtist::pointer res {session.getDboSession().add(std::make_unique<StarredArtist>(artist, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = normalizeDateTime(dateTime);
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#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> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredRelease>{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> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
StarredRelease::pointer res {session.getDboSession().add(std::make_unique<StarredRelease>(release, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = normalizeDateTime(dateTime);
|
||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#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> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<StarredTrack> {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> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
RangeResults<StarredTrackId>
|
||||
StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkSharedLocked();
|
||||
|
||||
StarredTrack::pointer res {session.getDboSession().add(std::make_unique<StarredTrack>(track, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
auto query {session.getDboSession().query<StarredTrackId>("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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ createQuery(Session& session, const Track::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<TrackId>("SELECT t.id from track t")};
|
||||
auto query {session.getDboSession().query<TrackId>(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<Track>(p))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
return session.getDboSession().add(std::unique_ptr<Track> {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<QueryResultType>("SELECT id, file_path FROM track")};
|
||||
|
||||
RangeResults<QueryResultType> queryResults {execQuery(query, range)};
|
||||
RangeResults<QueryResultType> queryResults {Utils::execQuery(query, range)};
|
||||
|
||||
RangeResults<PathResult> res;
|
||||
res.range = queryResults.range;
|
||||
@@ -193,7 +238,7 @@ Track::findRecordingMBIDDuplicates(Session& session, Range range)
|
||||
auto query {session.getDboSession().query<TrackId>( "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<TrackId>
|
||||
@@ -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<Cluster::pointer>
|
||||
@@ -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<TrackId>
|
||||
@@ -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<TrackId>
|
||||
@@ -278,7 +323,7 @@ Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, R
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -33,6 +33,12 @@ _track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackBookmark> {new TrackBookmark {user, track}});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackBookmark::getCount(Session& session)
|
||||
{
|
||||
@@ -41,18 +47,6 @@ TrackBookmark::getCount(Session& session)
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
|
||||
}
|
||||
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackBookmark::pointer res {session.getDboSession().add(std::make_unique<TrackBookmark>(user, track))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RangeResults<TrackBookmarkId>
|
||||
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<TrackBookmarkId>("SELECT id from track_bookmark")
|
||||
.where("user_id = ?").bind(userId)};
|
||||
|
||||
return execQuery(query, range);
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
|
||||
@@ -36,6 +36,12 @@ _track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<TrackFeatures> {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<TrackFeaturesId>("SELECT id from track_features")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackFeatures::pointer res {session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
FeatureValues
|
||||
|
||||
@@ -35,24 +35,21 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user)
|
||||
: _name {name},
|
||||
_type {type},
|
||||
_isPublic {isPublic},
|
||||
_user {getDboPtr(user)}
|
||||
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> 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> user)
|
||||
TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
assert(user);
|
||||
|
||||
TrackList::pointer res {session.getDboSession().add( std::make_unique<TrackList>(name, type, isPublic, user) )};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
return session.getDboSession().add(std::unique_ptr<TrackList> {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<TrackListId>
|
||||
TrackList::find(Session& session, UserId userId, Range range)
|
||||
TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist")
|
||||
.where("user_id = ?").bind(userId)
|
||||
.orderBy("name COLLATE NOCASE")};
|
||||
auto query {session.getDboSession().query<TrackListId>("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<TrackListId>
|
||||
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<TrackListId>("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> track, const Wt::WDateTim
|
||||
return session()->find<TrackListEntry>()
|
||||
.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<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
|
||||
if (clusterTypes.empty())
|
||||
return res;
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Cluster>>("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<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& 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<Artist::pointer>
|
||||
TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
@@ -580,23 +660,30 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> 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> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
assert(track);
|
||||
assert(tracklist);
|
||||
return session.getDboSession().add(std::unique_ptr<TrackListEntry>( new TrackListEntry {track, tracklist, dateTime}));
|
||||
}
|
||||
|
||||
auto res = session.getDboSession().add(std::make_unique<TrackListEntry>( 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
|
||||
|
||||
@@ -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<User> {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<User>().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<User>(loginName))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::find(Session& session, UserId id)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ class Artist : public Object<Artist, ArtistId>
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -89,13 +88,6 @@ class Artist : public Object<Artist, ArtistId>
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
RangeResults<ReleaseId> getReleases(Range range, const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
|
||||
RangeResults<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const;
|
||||
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
|
||||
|
||||
@@ -108,9 +100,6 @@ class Artist : public Object<Artist, ArtistId>
|
||||
void setMBID(const std::optional<UUID>& 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>& UUID = {});
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
@@ -125,6 +114,11 @@ class Artist : public Object<Artist, ArtistId>
|
||||
private:
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
friend class Session;
|
||||
// Create
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
@@ -36,10 +36,8 @@ namespace Database
|
||||
{
|
||||
public:
|
||||
AuthToken() = default;
|
||||
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
|
||||
// Utility
|
||||
static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||
static pointer find(Session& session, std::string_view value);
|
||||
|
||||
@@ -57,6 +55,10 @@ namespace Database
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||
|
||||
std::string _value;
|
||||
Wt::WDateTime _expiry;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
|
||||
@@ -42,7 +42,6 @@ class Cluster : public Object<Cluster, ClusterId>
|
||||
{
|
||||
public:
|
||||
Cluster() = default;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
// Find utility
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -50,9 +49,6 @@ class Cluster : public Object<Cluster, ClusterId>
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
@@ -72,6 +68,10 @@ class Cluster : public Object<Cluster, ClusterId>
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
@@ -85,7 +85,6 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
|
||||
{
|
||||
public:
|
||||
ClusterType() = default;
|
||||
ClusterType(std::string_view name);
|
||||
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -95,7 +94,6 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
|
||||
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
||||
|
||||
static pointer create(Session& session, const std::string& name);
|
||||
static void remove(Session& session, const std::string& name);
|
||||
|
||||
// Accessors
|
||||
@@ -112,6 +110,10 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -42,7 +42,6 @@ class Listen : public Object<Listen, ListenId>
|
||||
{
|
||||
public:
|
||||
Listen() = default;
|
||||
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
@@ -63,9 +62,6 @@ class Listen : public Object<Listen, ListenId>
|
||||
static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
|
||||
// Stats
|
||||
static RangeResults<ArtistId> getTopArtists(Session& session,
|
||||
UserId userId,
|
||||
@@ -120,6 +116,10 @@ class Listen : public Object<Listen, ListenId>
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
||||
|
||||
Wt::WDateTime _dateTime;
|
||||
Scrobbler _scrobbler;
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#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 <typename, typename> friend class Object;
|
||||
@@ -61,6 +67,14 @@ namespace Database
|
||||
typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete;
|
||||
|
||||
protected:
|
||||
template <typename> 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 <typename SomeObject>
|
||||
static
|
||||
|
||||
@@ -25,11 +25,13 @@
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#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<Release, ReleaseId>
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
std::optional<DateRange> dateRange;
|
||||
UserId starringUser; // only releases starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
UserId starringUser; // only releases starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
ArtistId artist; // only releases that involved this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
@@ -64,10 +69,16 @@ class Release : public Object<Release, ReleaseId>
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; }
|
||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) {dateRange = _dateRange; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Release() = default;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -79,18 +90,13 @@ class Release : public Object<Release, ReleaseId>
|
||||
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
||||
|
||||
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters = {}) const;
|
||||
std::size_t getTracksCount() const;
|
||||
ObjectPtr<Track> getFirstTrack() const;
|
||||
|
||||
// Get the cluster of the tracks that belong to this release
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Utility functions
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
@@ -123,6 +129,10 @@ class Release : public Object<Release, ReleaseId>
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
static const std::size_t _maxNameLength {128};
|
||||
|
||||
std::string _name;
|
||||
|
||||
@@ -39,7 +39,8 @@ class ScanSettings : public Object<ScanSettings, ScanSettingsId>
|
||||
{
|
||||
public:
|
||||
// Do not modify values (just add)
|
||||
enum class UpdatePeriod {
|
||||
enum class UpdatePeriod
|
||||
{
|
||||
Never = 0,
|
||||
Daily,
|
||||
Weekly,
|
||||
|
||||
@@ -19,16 +19,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class UniqueTransaction
|
||||
{
|
||||
private:
|
||||
@@ -73,11 +72,24 @@ namespace Database
|
||||
Wt::Dbo::Session& getDboSession() { return _session; }
|
||||
Db& getDb() { return _db; }
|
||||
|
||||
template <typename Object, typename... Args>
|
||||
typename Object::pointer create(Args&&... args)
|
||||
{
|
||||
checkUniqueLocked();
|
||||
|
||||
typename Object::pointer res {Object::create(*this, std::forward<Args>(args)...)};
|
||||
getDboSession().flush();
|
||||
|
||||
if (res->hasOnPostCreated())
|
||||
res.modify()->onPostCreated();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
Wt::Dbo::Session _session;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
|
||||
@@ -23,14 +23,11 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/StarredArtistId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredArtistId)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
@@ -41,37 +38,41 @@ namespace Database
|
||||
{
|
||||
public:
|
||||
StarredArtist() = default;
|
||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredArtistId)
|
||||
|
||||
@@ -22,15 +22,12 @@
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/StarredReleaseId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredReleaseId)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Release;
|
||||
@@ -41,37 +38,41 @@ namespace Database
|
||||
{
|
||||
public:
|
||||
StarredRelease() = default;
|
||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredReleaseId)
|
||||
|
||||
@@ -23,13 +23,11 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/StarredTrackId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredTrackId)
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -41,37 +39,54 @@ namespace Database
|
||||
{
|
||||
public:
|
||||
StarredTrack() = default;
|
||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Scrobbler> scrobbler; // for this scrobbler
|
||||
std::optional<ScrobblingState> scrobblingState; // and these states
|
||||
UserId user; // and this user
|
||||
Range range;
|
||||
|
||||
FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; }
|
||||
FindParameters& setUser(UserId _user) {user = _user; return *this; }
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
};
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||
|
||||
// Setters
|
||||
void setDateTime(const Wt::WDateTime& dateTime);
|
||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
||||
|
||||
Scrobbler _scrobbler; // for which scrobbler
|
||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
||||
Wt::WDateTime _dateTime; // when it was starred
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(StarredTrackId)
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/TrackListId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
@@ -63,7 +65,13 @@ class Track : public Object<Track, TrackId>
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only tracks starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
ArtistId artist; // only tracks that involve this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
bool nonRelease {}; // only tracks that do not belong to a release
|
||||
ReleaseId release; // matching this release
|
||||
TrackListId trackList; // matching this trackList
|
||||
bool distinct {true};
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
@@ -71,7 +79,13 @@ class Track : public Object<Track, TrackId>
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
||||
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; }
|
||||
FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; }
|
||||
};
|
||||
|
||||
struct PathResult
|
||||
{
|
||||
TrackId trackId;
|
||||
@@ -79,7 +93,6 @@ class Track : public Object<Track, TrackId>
|
||||
};
|
||||
|
||||
Track() = default;
|
||||
Track(const std::filesystem::path& p);
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -95,9 +108,6 @@ class Track : public Object<Track, TrackId>
|
||||
static RangeResults<TrackId> findRecordingMBIDDuplicates(Session& session, Range range);
|
||||
static RangeResults<TrackId> 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; }
|
||||
@@ -184,6 +194,10 @@ class Track : public Object<Track, TrackId>
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace Database
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _type, "name");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -62,7 +61,6 @@ namespace Database
|
||||
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _name;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
|
||||
@@ -41,10 +41,6 @@ class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
|
||||
{
|
||||
public:
|
||||
TrackBookmark () = default;
|
||||
TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -70,7 +66,12 @@ class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
|
||||
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> user, ObjectPtr<Track> track);
|
||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
|
||||
|
||||
static const std::size_t _maxCommentLength = 128;
|
||||
|
||||
std::chrono::duration<int, std::milli> _offset;
|
||||
|
||||
@@ -46,7 +46,6 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
||||
{
|
||||
public:
|
||||
TrackFeatures() = default;
|
||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
// Find utilities
|
||||
static std::size_t getCount(Session& session);
|
||||
@@ -54,9 +53,6 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
||||
static pointer find(Session& session, TrackId trackId);
|
||||
static RangeResults<TrackFeaturesId> find(Session& session, Range range);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
||||
|
||||
@@ -71,6 +67,10 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
|
||||
std::string _data;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Database {
|
||||
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
@@ -47,14 +48,7 @@ class User;
|
||||
class TrackList : public Object<TrackList, TrackListId>
|
||||
{
|
||||
public:
|
||||
enum class Type
|
||||
{
|
||||
Playlist, // user controlled playlists
|
||||
Internal, // internal usage (current playqueue, history, ...)
|
||||
};
|
||||
|
||||
TrackList() = default;
|
||||
TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
||||
|
||||
// Stats utility
|
||||
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
@@ -62,19 +56,29 @@ class TrackList : public Object<TrackList, TrackListId>
|
||||
std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, std::string_view name, Type type, UserId userId);
|
||||
static pointer find(Session& session, TrackListId tracklistId);
|
||||
static RangeResults<TrackListId> find(Session& session, UserId userId, Range range);
|
||||
static RangeResults<TrackListId> find(Session& session, UserId userId, Type type, Range range);
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
||||
Range range;
|
||||
std::optional<TrackListType> type;
|
||||
UserId user; // only tracklists owned by this user
|
||||
TrackListSortMethod sortMethod {TrackListSortMethod::None};
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _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<TrackListId> find(Session& session, const FindParameters& params);
|
||||
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
bool isPublic() const { return _isPublic; }
|
||||
Type getType() const { return _type; }
|
||||
TrackListType getType() const { return _type; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
|
||||
// Modifiers
|
||||
@@ -101,8 +105,11 @@ class TrackList : public Object<TrackList, TrackListId>
|
||||
std::vector<TrackId> getTrackIds() const;
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
|
||||
void setLastModifiedDateTime(const Wt::WDateTime& dateTime);
|
||||
|
||||
// Get clusters, order by occurence
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
|
||||
bool hasTrack(TrackId trackId) const;
|
||||
|
||||
@@ -112,18 +119,26 @@ class TrackList : public Object<TrackList, TrackListId>
|
||||
template<class Action>
|
||||
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> user);
|
||||
static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
|
||||
|
||||
std::string _name;
|
||||
Type _type {Type::Playlist};
|
||||
TrackListType _type {TrackListType::Playlist};
|
||||
bool _isPublic {false};
|
||||
Wt::WDateTime _creationDateTime;
|
||||
Wt::WDateTime _lastModifiedDateTime;
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
|
||||
@@ -133,15 +148,16 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
|
||||
{
|
||||
public:
|
||||
TrackListEntry() = default;
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
|
||||
|
||||
bool hasOnPostCreated() const override { return true; }
|
||||
void onPostCreated() override;
|
||||
|
||||
bool hasOnPreRemove() const override { return true; }
|
||||
void onPreRemove() override;
|
||||
|
||||
// find utility
|
||||
static pointer getById(Session& session, TrackListEntryId id);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
|
||||
|
||||
// Accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||
@@ -156,6 +172,11 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
|
||||
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
|
||||
|
||||
Wt::WDateTime _dateTime; // optional date time
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<TrackList> _tracklist;
|
||||
|
||||
@@ -98,17 +98,29 @@ namespace Database
|
||||
None,
|
||||
Name,
|
||||
Date,
|
||||
DateDesc,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
};
|
||||
|
||||
enum class TrackListSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
LastModifiedDesc,
|
||||
};
|
||||
|
||||
enum class TrackSortMethod
|
||||
{
|
||||
None,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
Name,
|
||||
DateDescAndRelease,
|
||||
Release, // order by disc/track number
|
||||
TrackList, // order by asc order in tracklist
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkType
|
||||
@@ -173,5 +185,11 @@ namespace Database
|
||||
ReleaseArtists = 1,
|
||||
TrackArtists = 2,
|
||||
};
|
||||
|
||||
enum class TrackListType
|
||||
{
|
||||
Playlist, // user controlled playlists
|
||||
Internal, // internal usage (current playqueue, history, ...)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,6 @@ class User : public Object<User, UserId>
|
||||
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<User, UserId>
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -80,8 +80,6 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
EXPECT_EQ(artist->getReleaseCount(), 0);
|
||||
|
||||
ASSERT_EQ(track->getArtistLinks().size(), 1);
|
||||
auto artistLink {track->getArtistLinks().front()};
|
||||
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
|
||||
@@ -91,17 +89,6 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty());
|
||||
EXPECT_EQ(track->getArtists({}).size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto tracks {artist->getTracks()};
|
||||
ASSERT_EQ(tracks.size(), 1);
|
||||
EXPECT_EQ(tracks.front()->getId(), track.getId());
|
||||
|
||||
EXPECT_TRUE(artist->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
|
||||
EXPECT_EQ(artist->getTracks(TrackArtistLinkType::Artist).size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
@@ -143,10 +130,18 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
|
||||
EXPECT_EQ(track->getArtistLinks().size(), 3);
|
||||
|
||||
EXPECT_EQ(artist->getTracks().size(), 1);
|
||||
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::ReleaseArtist}).size(), 1);
|
||||
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::Artist}).size(), 1);
|
||||
EXPECT_EQ(artist->getTracks({TrackArtistLinkType::Writer}).size(), 1);
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +182,25 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
EXPECT_EQ(artist1->getTracks().front(), track.get());
|
||||
EXPECT_EQ(artist2->getTracks().front(), track.get());
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
EXPECT_TRUE(artist1->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
|
||||
EXPECT_EQ(artist1->getTracks(TrackArtistLinkType::Artist).size(), 1);
|
||||
EXPECT_TRUE(artist2->getTracks(TrackArtistLinkType::ReleaseArtist).empty());
|
||||
EXPECT_EQ(artist2->getTracks(TrackArtistLinkType::Artist).size(), 1);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId()));
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track->getId());
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist}));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,9 +331,8 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt));
|
||||
|
||||
const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})};
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -339,15 +345,12 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})};
|
||||
EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt));
|
||||
EXPECT_FALSE(tracks.moreResults);
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist->getId()).setNonRelease(true))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front()->getId(), track2.getId());
|
||||
EXPECT_EQ(tracks.results.front(), track2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
|
||||
ScopedRelease releaseC {session, "ReleaseC"};
|
||||
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "TrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "TrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
@@ -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<TrackListEntry>(trackA.get(), trackList.get());
|
||||
session.create<TrackListEntry>(trackB.get(), trackList.get());
|
||||
session.create<TrackListEntry>(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<TrackListEntry>(trackC.get(), trackList.get());
|
||||
session.create<TrackListEntry>(trackC.get(), trackList.get());
|
||||
session.create<TrackListEntry>(trackC.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -574,11 +574,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = artist->getReleases(Range {}, {cluster.getId()});
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId()).setClusters({cluster.getId()}));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -605,11 +605,11 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = artist->getReleases(Range {}, {cluster1.getId(), cluster2.getId()});
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId()).setClusters({cluster1.getId(), cluster2.getId()}));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
}
|
||||
@@ -618,7 +618,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||
std::list<ScopedTrack> tracks;
|
||||
@@ -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<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
|
||||
if (i < 10)
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
@@ -650,7 +650,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
||||
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
|
||||
@@ -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<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
|
||||
if (i < 10)
|
||||
{
|
||||
@@ -702,7 +702,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
||||
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
|
||||
@@ -742,7 +742,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track1.get(), trackList.get(), now);
|
||||
session.create<TrackListEntry>(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<TrackListEntry>(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<TrackListEntry>(track1.get(), trackList.get(), now.addSecs(2));
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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>(args)...)};
|
||||
auto entity {_session.create<T>(std::forward<Args>(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;
|
||||
|
||||
@@ -49,12 +49,12 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_EQ(artist->getReleaseCount(), 1);
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
EXPECT_EQ(release->getTracks().size(), nbTracks);
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(tracks.results.size(), nbTracks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +74,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId()))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
EXPECT_EQ(artist->getReleaseCount(), 1);
|
||||
|
||||
auto artists {release->getArtists()};
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist.getId());
|
||||
|
||||
@@ -70,9 +70,9 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty());
|
||||
|
||||
EXPECT_EQ(release->getTracksCount(), 1);
|
||||
ASSERT_EQ(release->getTracks().size(), 1);
|
||||
EXPECT_EQ(release->getTracks().front()->getId(), track.getId());
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -103,7 +103,8 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
EXPECT_TRUE(release->getTracks().empty());
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
|
||||
auto releases {Release::findOrphans(session, Range {})};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -279,8 +280,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_FALSE(release1->getFirstTrack());
|
||||
EXPECT_FALSE(release2->getFirstTrack());
|
||||
EXPECT_TRUE(Track::find(session, Track::FindParameters {}.setRelease(release1.getId())).results.empty());
|
||||
EXPECT_TRUE(Track::find(session, Track::FindParameters {}.setRelease(release2.getId())).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -303,11 +304,17 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
EXPECT_TRUE(release1->getFirstTrack());
|
||||
EXPECT_TRUE(release2->getFirstTrack());
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release))};
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
EXPECT_EQ(tracks.results.front(), track1A.getId());
|
||||
}
|
||||
|
||||
EXPECT_EQ(release1->getFirstTrack()->getId(), track1A.getId());
|
||||
EXPECT_EQ(release2->getFirstTrack()->getId(), track2B.getId());
|
||||
{
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release))};
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
EXPECT_EQ(tracks.results.front(), track2B.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,4 +403,64 @@ TEST_F(DatabaseFixture, Release_writtenAfter)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_artist)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedArtist artist2 {session, "MyArtist2"};
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))};
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))};
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer}));
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId()));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist}));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId()));
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
EXPECT_EQ(releases.results.front(), release.getId());
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
|
||||
releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist, TrackArtistLinkType::Composer}));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,24 @@ TEST_F(DatabaseFixture, StarredArtist)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||
{
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
|
||||
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||
{
|
||||
ScopedArtist artist1 {session, "MyArtist1"};
|
||||
|
||||
@@ -64,6 +64,24 @@ TEST_F(DatabaseFixture, StarredRelease)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
|
||||
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||
{
|
||||
ScopedRelease release1 {session, "MyRelease1"};
|
||||
|
||||
@@ -64,6 +64,24 @@ TEST_F(DatabaseFixture, StarredTrack)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))};
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||
{
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
|
||||
@@ -31,7 +31,7 @@ TEST_F(DatabaseFixture, SingleTrackList)
|
||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
@@ -39,10 +39,94 @@ TEST_F(DatabaseFixture, SingleTrackList)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId()));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
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()};
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}});
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}});
|
||||
}
|
||||
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}});
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}});
|
||||
}
|
||||
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
std::list<ScopedTrack> tracks;
|
||||
|
||||
for (std::size_t i {}; i < 10; ++i)
|
||||
@@ -50,7 +134,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<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -84,7 +168,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedTrack track3 {session, "MyTrack3"};
|
||||
@@ -92,9 +176,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<TrackListEntry>(track1.get(), trackList.get(), now);
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(-1));
|
||||
session.create<TrackListEntry>(track3.get(), trackList.get(), now.addSecs(1));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -111,7 +195,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
|
||||
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack1"};
|
||||
ScopedArtist artist1 {session, "MyArtist1"};
|
||||
@@ -141,7 +225,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track1.get(), trackList.get(), now);
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get(), now);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -163,7 +247,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1));
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(1));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -189,7 +273,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2));
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get(), now.addSecs(2));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -216,7 +300,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
|
||||
TEST_F(DatabaseFixture, SingleTrackList_getArtists)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
@@ -235,7 +319,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<TrackListEntry>(track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -268,7 +352,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<TrackListEntry>(track2.get(), trackList.get());
|
||||
artist1.get().modify()->setSortName("ZZZ");
|
||||
artist2.get().modify()->setSortName("AAA");
|
||||
}
|
||||
@@ -297,7 +381,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists)
|
||||
TEST_F(DatabaseFixture, SingleTrackList_getReleases)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
|
||||
@@ -315,7 +399,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getReleases)
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
|
||||
TrackListEntry::create(session, track.get(), trackList.get());
|
||||
session.create<TrackListEntry>(track.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -332,7 +416,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getReleases)
|
||||
TEST_F(DatabaseFixture, SingleTrackList_getTracks)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack1"};
|
||||
|
||||
@@ -347,9 +431,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<TrackListEntry>(track1.get(), trackList.get());
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -363,3 +447,35 @@ TEST_F(DatabaseFixture, SingleTrackList_getTracks)
|
||||
ASSERT_EQ(tracks[1]->getId(), track2->getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
||||
{
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()};
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))};
|
||||
EXPECT_EQ(trackLists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))};
|
||||
ASSERT_EQ(trackLists.results.size(), 1);
|
||||
EXPECT_EQ(trackLists.results.front(), trackList1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Artist>(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<Release>(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<Release>(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<Cluster>(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<TrackFeatures>(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<Track>(file);
|
||||
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'";
|
||||
stats.additions++;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
add_library(lmsscrobbling SHARED
|
||||
impl/internal/InternalScrobbler.cpp
|
||||
impl/listenbrainz/FeedbacksSynchronizer.cpp
|
||||
impl/listenbrainz/ListenBrainzScrobbler.cpp
|
||||
impl/listenbrainz/ListensSynchronizer.cpp
|
||||
impl/listenbrainz/Utils.cpp
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackListId.hpp"
|
||||
#include "services/database/StarredArtistId.hpp"
|
||||
#include "services/database/StarredReleaseId.hpp"
|
||||
#include "services/database/StarredTrackId.hpp"
|
||||
#include "services/scrobbling/Listen.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -47,16 +47,13 @@ namespace Scrobbling
|
||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
|
||||
// Feedback
|
||||
virtual void onStarred(Database::UserId, Database::ArtistId) {};
|
||||
virtual void onUnstarred(Database::UserId, Database::ArtistId) {};
|
||||
virtual void onStarred(Database::UserId, Database::ReleaseId) {};
|
||||
virtual void onUnstarred(Database::UserId, Database::ReleaseId) {};
|
||||
virtual void onStarred(Database::UserId, Database::TrackId) {};
|
||||
virtual void onUnstarred(Database::UserId, Database::TrackId) {};
|
||||
|
||||
// virtual void star(Database::TrackId trackId) = 0;
|
||||
// virtual void unstar(Database::TrackId trackId) = 0;
|
||||
// Feedbacks
|
||||
virtual void onStarred(Database::StarredArtistId) = 0;
|
||||
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onUnstarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onStarred(Database::StarredTrackId) = 0;
|
||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace Scrobbling
|
||||
if (!scrobbler)
|
||||
return;
|
||||
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
@@ -50,11 +51,12 @@ namespace Scrobbling
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
starredObj = StarredObjType::create(session, obj, user, *scrobbler);
|
||||
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
|
||||
}
|
||||
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onStarred(userId, objId);
|
||||
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
@@ -65,14 +67,18 @@ namespace Scrobbling
|
||||
if (!scrobbler)
|
||||
return;
|
||||
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
if (typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)})
|
||||
starredObj.remove();
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
if (!starredObj)
|
||||
return;
|
||||
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onUnstarred(userId, objId);
|
||||
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
@@ -86,7 +92,8 @@ namespace Scrobbling
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
return StarredObjType::find(session, objId, userId, *scrobbler);
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
|
||||
}
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
@@ -22,8 +22,32 @@
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Listen.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/database/StarredArtist.hpp"
|
||||
#include "services/database/StarredRelease.hpp"
|
||||
#include "services/database/StarredTrack.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
starredObj.remove();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
@@ -64,8 +88,43 @@ namespace Scrobbling
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
auto dbListen {Database::Listen::create(session, user, track, Database::Scrobbler::Internal, listen.listenedAt)};
|
||||
auto dbListen {session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt)};
|
||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
} // Scrobbling
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ namespace Scrobbling
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
void onStarred(Database::StarredArtistId) override;
|
||||
void onUnstarred(Database::StarredArtistId) override;
|
||||
void onStarred(Database::StarredReleaseId) override;
|
||||
void onUnstarred(Database::StarredReleaseId) override;
|
||||
void onStarred(Database::StarredTrackId) override;
|
||||
void onUnstarred(Database::StarredTrackId) override;
|
||||
|
||||
Database::Db& _db;
|
||||
};
|
||||
} // Scrobbling
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
|
||||
enum class FeedbackType
|
||||
{
|
||||
Love = 1,
|
||||
Hate = -1,
|
||||
Erase = 0,
|
||||
};
|
||||
|
||||
struct Feedback
|
||||
{
|
||||
Wt::WDateTime created;
|
||||
UUID recordingMBID;
|
||||
FeedbackType score;
|
||||
};
|
||||
} // Scrobbling::ListenBrainz
|
||||
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ListenBrainzScrobbler.hpp"
|
||||
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <Wt/Json/Array.h>
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
#include <Wt/Json/Serializer.h>
|
||||
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/StarredTrack.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/scrobbling/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/http/IClient.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Scrobbling::ListenBrainz;
|
||||
using namespace Database;
|
||||
|
||||
namespace
|
||||
{
|
||||
class Exception : public Scrobbling::Exception
|
||||
{
|
||||
public:
|
||||
using Scrobbling::Exception::Exception;
|
||||
};
|
||||
|
||||
class ParseErrorException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class MBIDNotFoundException : public Exception
|
||||
{
|
||||
public:
|
||||
MBIDNotFoundException() : Exception {"MBID not found"} {}
|
||||
};
|
||||
|
||||
std::optional<std::size_t>
|
||||
parseTotalFeedbackCount(std::string_view msgBody)
|
||||
{
|
||||
try
|
||||
{
|
||||
Wt::Json::Object root;
|
||||
Wt::Json::parse(std::string {msgBody}, root);
|
||||
|
||||
return static_cast<int>(root.get("total_count"));
|
||||
}
|
||||
catch (const Wt::WException& e)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
Feedback
|
||||
parseFeedback(const Wt::Json::Object& feedbackObj)
|
||||
{
|
||||
try
|
||||
{
|
||||
const std::optional<UUID> recordingMBID {UUID::fromString(static_cast<std::string>(feedbackObj.get("recording_mbid")))};
|
||||
if (!recordingMBID)
|
||||
throw MBIDNotFoundException {};
|
||||
|
||||
return Feedback
|
||||
{
|
||||
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
|
||||
*recordingMBID,
|
||||
static_cast<FeedbackType>(static_cast<int>(feedbackObj.get("score")))
|
||||
};
|
||||
}
|
||||
catch (const Wt::WException& e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what();
|
||||
throw Exception {};
|
||||
}
|
||||
}
|
||||
|
||||
struct GetFeedbacksResult
|
||||
{
|
||||
std::size_t totalFeedbackCount{};
|
||||
std::vector<Feedback> feedbacks;
|
||||
};
|
||||
|
||||
GetFeedbacksResult
|
||||
parseGetFeedbacks(std::string_view msgBody)
|
||||
{
|
||||
GetFeedbacksResult res;
|
||||
|
||||
try
|
||||
{
|
||||
Wt::Json::Object root;
|
||||
Wt::Json::parse(std::string {msgBody}, root);
|
||||
|
||||
const Wt::Json::Array& feedbacks = root.get("feedback");
|
||||
|
||||
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
|
||||
|
||||
if (feedbacks.empty())
|
||||
return res;
|
||||
|
||||
res.totalFeedbackCount += feedbacks.size();
|
||||
|
||||
for (const Wt::Json::Value& value : feedbacks)
|
||||
{
|
||||
try
|
||||
{
|
||||
res.feedbacks.push_back(parseFeedback(value));
|
||||
}
|
||||
catch (const Exception &e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const Wt::WException& error)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'get-feedback' result: " << error.what();
|
||||
throw ParseErrorException {error.what()};
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client)
|
||||
: _ioContext {ioContext}
|
||||
, _db {db}
|
||||
, _client {client}
|
||||
, _maxSyncFeedbackCount {Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000)}
|
||||
, _syncFeedbacksPeriod {Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1)}
|
||||
{
|
||||
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
|
||||
|
||||
scheduleSync(std::chrono::seconds {30});
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)};
|
||||
if (!starredTrack)
|
||||
return;
|
||||
|
||||
std::optional<UUID> recordingMBID {starredTrack->getTrack()->getRecordingMBID()};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FeedbackType::Love:
|
||||
starredTrack.modify()->setScrobblingState(ScrobblingState::PendingAdd);
|
||||
break;
|
||||
|
||||
case FeedbackType::Erase:
|
||||
if (!recordingMBID)
|
||||
{
|
||||
LOG(DEBUG) << "Track has no recording MBID: erasing star";
|
||||
starredTrack.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send the erase order even if it is not on the remote LB server (it may be
|
||||
// queued for add, or not)
|
||||
starredTrack.modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception {"Unhandled feedback type"};
|
||||
}
|
||||
|
||||
const std::optional<UUID> listenBrainzToken {starredTrack->getUser()->getListenBrainzToken()};
|
||||
if (!listenBrainzToken)
|
||||
return;
|
||||
|
||||
Http::ClientPOSTRequestParameters request;
|
||||
request.relativeUrl = "/1/feedback/recording-feedback";
|
||||
request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
|
||||
|
||||
Wt::Json::Object root;
|
||||
root["recording_mbid"] = Wt::Json::Value {std::string {recordingMBID->getAsString()}};
|
||||
root["score"] = Wt::Json::Value {static_cast<int>(type)};
|
||||
|
||||
request.message.addBodyText(Wt::Json::serialize(root));
|
||||
request.message.addHeader("Content-Type", "application/json");
|
||||
|
||||
request.onSuccessFunc = [=](std::string_view /*msgBody*/)
|
||||
{
|
||||
_strand.dispatch([=]
|
||||
{
|
||||
onFeedbackSent(type, starredTrackId);
|
||||
});
|
||||
};
|
||||
_client.sendPOSTRequest(std::move(request));
|
||||
}
|
||||
catch (Exception& e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot send feedback: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
assert(_strand.running_in_this_thread());
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)};
|
||||
if (!starredTrack)
|
||||
{
|
||||
LOG(DEBUG) << "Starred track not found. deleted?";
|
||||
return;
|
||||
}
|
||||
|
||||
UserContext& userContext {getUserContext(starredTrack->getUser()->getId())};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FeedbackType::Love:
|
||||
starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized);
|
||||
LOG(DEBUG) << "State set to synchronized";
|
||||
|
||||
if (userContext.feedbackCount)
|
||||
{
|
||||
(*userContext.feedbackCount)++;
|
||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'";
|
||||
}
|
||||
break;
|
||||
|
||||
case FeedbackType::Erase:
|
||||
starredTrack.remove();
|
||||
LOG(DEBUG) << "Removed starred track";
|
||||
|
||||
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
|
||||
{
|
||||
(*userContext.feedbackCount)--;
|
||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception {"Unhandled feedback type"};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::enquePendingFeedbacks()
|
||||
{
|
||||
auto processPendingFeedbacks { [this] (ScrobblingState scrobblingState, FeedbackType feedbackType)
|
||||
{
|
||||
RangeResults<StarredTrackId> pendingFeedbacks;
|
||||
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
StarredTrack::FindParameters params;
|
||||
params.setScrobbler(Database::Scrobbler::ListenBrainz, scrobblingState)
|
||||
.setRange(Database::Range {0, 100}); // don't flood too much?
|
||||
|
||||
pendingFeedbacks = StarredTrack::find(session, params);
|
||||
}
|
||||
|
||||
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
|
||||
|
||||
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
|
||||
enqueFeedback(feedbackType, starredTrackId);
|
||||
}};
|
||||
|
||||
processPendingFeedbacks(ScrobblingState::PendingAdd, FeedbackType::Love);
|
||||
processPendingFeedbacks(ScrobblingState::PendingRemove, FeedbackType::Erase);
|
||||
}
|
||||
|
||||
FeedbacksSynchronizer::UserContext&
|
||||
FeedbacksSynchronizer::getUserContext(Database::UserId userId)
|
||||
{
|
||||
assert(_strand.running_in_this_thread());
|
||||
|
||||
auto itContext {_userContexts.find(userId)};
|
||||
if (itContext == std::cend(_userContexts))
|
||||
{
|
||||
auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
|
||||
itContext = itNewContext;
|
||||
}
|
||||
|
||||
return itContext->second;
|
||||
}
|
||||
|
||||
bool
|
||||
FeedbacksSynchronizer::isSyncing() const
|
||||
{
|
||||
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
|
||||
{
|
||||
const auto& [userId, context] {contextEntry};
|
||||
return context.syncing;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow)
|
||||
{
|
||||
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
|
||||
return;
|
||||
|
||||
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
|
||||
_syncTimer.expires_after(fromNow);
|
||||
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec)
|
||||
{
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
LOG(DEBUG) << "getFeedbacks aborted";
|
||||
return;
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
throw Exception {"GetFeedbacks timer failure: " + std::string {ec.message()} };
|
||||
}
|
||||
|
||||
startSync();
|
||||
}));
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::startSync()
|
||||
{
|
||||
LOG(DEBUG) << "Starting sync!";
|
||||
|
||||
assert(!isSyncing());
|
||||
assert(_strand.running_in_this_thread());
|
||||
|
||||
enquePendingFeedbacks();
|
||||
|
||||
Database::RangeResults<Database::UserId> userIds;
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz));
|
||||
}
|
||||
|
||||
for (const Database::UserId userId : userIds.results)
|
||||
startSync(getUserContext(userId));
|
||||
|
||||
if (!isSyncing())
|
||||
scheduleSync(_syncFeedbacksPeriod);
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::startSync(UserContext& context)
|
||||
{
|
||||
context.syncing = true;
|
||||
context.listenBrainzUserName = "";
|
||||
context.fetchedFeedbackCount = 0;
|
||||
context.matchedFeedbackCount = 0;
|
||||
context.importedFeedbackCount = 0;
|
||||
|
||||
enqueValidateToken(context);
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::onSyncEnded(UserContext& context)
|
||||
{
|
||||
_strand.dispatch([this, &context]
|
||||
{
|
||||
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
|
||||
context.syncing = false;
|
||||
|
||||
if (!isSyncing())
|
||||
scheduleSync(_syncFeedbacksPeriod);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::enqueValidateToken(UserContext& context)
|
||||
{
|
||||
assert(context.listenBrainzUserName.empty());
|
||||
|
||||
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)};
|
||||
if (!listenBrainzToken)
|
||||
{
|
||||
onSyncEnded(context);
|
||||
return;
|
||||
}
|
||||
|
||||
Http::ClientGETRequestParameters request;
|
||||
request.priority = Http::ClientRequestParameters::Priority::Low;
|
||||
request.relativeUrl = "/1/validate-token";
|
||||
request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
|
||||
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
|
||||
{
|
||||
context.listenBrainzUserName = Utils::parseValidateToken(msgBody);
|
||||
if (context.listenBrainzUserName.empty())
|
||||
{
|
||||
onSyncEnded(context);
|
||||
return;
|
||||
}
|
||||
enqueGetFeedbackCount(context);
|
||||
};
|
||||
request.onFailureFunc = [this, &context]
|
||||
{
|
||||
onSyncEnded(context);
|
||||
};
|
||||
|
||||
_client.sendGETRequest(std::move(request));
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::enqueGetFeedbackCount(UserContext& context)
|
||||
{
|
||||
assert(!context.listenBrainzUserName.empty());
|
||||
|
||||
Http::ClientGETRequestParameters request;
|
||||
request.relativeUrl = "/1/feedback/user/" + std::string {context.listenBrainzUserName} + "/get-feedback?score=1&count=0";
|
||||
request.priority = Http::ClientRequestParameters::Priority::Low;
|
||||
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
|
||||
{
|
||||
std::string msgBodyCopy {msgBody};
|
||||
_strand.dispatch([this, msgBodyCopy, &context]
|
||||
{
|
||||
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
|
||||
|
||||
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
|
||||
if (totalFeedbackCount)
|
||||
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
|
||||
|
||||
bool needSync {totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount)};
|
||||
context.feedbackCount = totalFeedbackCount;
|
||||
|
||||
if (needSync)
|
||||
enqueGetFeedbacks(context);
|
||||
else
|
||||
onSyncEnded(context);
|
||||
});
|
||||
};
|
||||
request.onFailureFunc = [this, &context]
|
||||
{
|
||||
onSyncEnded(context);
|
||||
};
|
||||
|
||||
_client.sendGETRequest(std::move(request));
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::enqueGetFeedbacks(UserContext& context)
|
||||
{
|
||||
assert(!context.listenBrainzUserName.empty());
|
||||
|
||||
Http::ClientGETRequestParameters request;
|
||||
request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount);
|
||||
request.priority = Http::ClientRequestParameters::Priority::Low;
|
||||
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
|
||||
{
|
||||
std::string msgBodyCopy {msgBody};
|
||||
_strand.dispatch([this, msgBodyCopy, &context]
|
||||
{
|
||||
try
|
||||
{
|
||||
const std::size_t fetchedFeedbackCount {processGetFeedbacks(msgBodyCopy, context)};
|
||||
if (fetchedFeedbackCount == 0 // no more thing available on server
|
||||
|| context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time
|
||||
|| context.fetchedFeedbackCount >= _maxSyncFeedbackCount)
|
||||
{
|
||||
onSyncEnded(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
enqueGetFeedbacks(context);
|
||||
}
|
||||
}
|
||||
catch (const Exception& e)
|
||||
{
|
||||
onSyncEnded(context);
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onFailureFunc = [=, &context]
|
||||
{
|
||||
onSyncEnded(context);
|
||||
};
|
||||
|
||||
_client.sendGETRequest(std::move(request));
|
||||
}
|
||||
|
||||
std::size_t
|
||||
FeedbacksSynchronizer::processGetFeedbacks(std::string_view msgBody, UserContext& context)
|
||||
{
|
||||
const GetFeedbacksResult parseResult {parseGetFeedbacks(msgBody)};
|
||||
|
||||
LOG(DEBUG) << "Parsed " << parseResult.totalFeedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
|
||||
context.fetchedFeedbackCount += parseResult.totalFeedbackCount;
|
||||
|
||||
for (const Feedback& feedback : parseResult.feedbacks)
|
||||
{
|
||||
tryImportFeedback(feedback, context);
|
||||
}
|
||||
|
||||
return parseResult.totalFeedbackCount;
|
||||
}
|
||||
|
||||
void
|
||||
FeedbacksSynchronizer::tryImportFeedback(const Feedback& feedback, UserContext& context)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
bool needImport{};
|
||||
TrackId trackId;
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const std::vector<Track::pointer> tracks {Track::findByRecordingMBID(session, feedback.recordingMBID)};
|
||||
if (tracks.size() > 1)
|
||||
{
|
||||
LOG(DEBUG) << "Duplicate recording MBIDs found for '" << feedback.recordingMBID.getAsString() << "', using first entry found";
|
||||
}
|
||||
else if (tracks.empty())
|
||||
{
|
||||
LOG(DEBUG) << "No track found for recording MBID '" << feedback.recordingMBID.getAsString() << "'";
|
||||
return;
|
||||
}
|
||||
|
||||
trackId = tracks.front()->getId();
|
||||
|
||||
const StarredTrack::pointer starredTrack {StarredTrack::find(session, trackId, context.userId, Database::Scrobbler::ListenBrainz)};
|
||||
needImport = !starredTrack;
|
||||
|
||||
// don't update starred date time
|
||||
// no need to update state if it was found as not synchronized
|
||||
// pending remove => will be removed later
|
||||
// pending add => will be resent later
|
||||
}
|
||||
|
||||
if (needImport)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
const Track::pointer track {Track::find(session, trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
const User::pointer user {User::find(session, context.userId)};
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
StarredTrack::pointer starredTrack {session.create<StarredTrack>(track, user, Database::Scrobbler::ListenBrainz)};
|
||||
starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized);
|
||||
|
||||
context.importedFeedbackCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.matchedFeedbackCount++;
|
||||
}
|
||||
}
|
||||
} // namespace Scrobbling::ListenBrainz
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/io_context_strand.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
#include "services/scrobbling/Listen.hpp"
|
||||
|
||||
#include "FeedbackTypes.hpp"
|
||||
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Http
|
||||
{
|
||||
class IClient;
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
class FeedbacksSynchronizer
|
||||
{
|
||||
public:
|
||||
FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client);
|
||||
|
||||
void enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId);
|
||||
|
||||
private:
|
||||
void onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId);
|
||||
|
||||
void enquePendingFeedbacks();
|
||||
|
||||
struct UserContext
|
||||
{
|
||||
UserContext(Database::UserId id) : userId {id} {}
|
||||
|
||||
UserContext(const UserContext&) = delete;
|
||||
UserContext(UserContext&&) = delete;
|
||||
UserContext& operator=(const UserContext&) = delete;
|
||||
UserContext& operator=(UserContext&&) = delete;
|
||||
|
||||
const Database::UserId userId;
|
||||
bool syncing {};
|
||||
std::optional<std::size_t> feedbackCount {};
|
||||
|
||||
// resetted at each sync
|
||||
std::string listenBrainzUserName; // need to be resolved first
|
||||
|
||||
std::size_t currentOffset{};
|
||||
std::size_t fetchedFeedbackCount{};
|
||||
std::size_t matchedFeedbackCount{};
|
||||
std::size_t importedFeedbackCount{};
|
||||
};
|
||||
|
||||
UserContext& getUserContext(Database::UserId userId);
|
||||
bool isSyncing() const;
|
||||
void scheduleSync(std::chrono::seconds fromNow);
|
||||
void startSync();
|
||||
void startSync(UserContext& context);
|
||||
void onSyncEnded(UserContext& context);
|
||||
void enqueValidateToken(UserContext& context);
|
||||
void enqueGetFeedbackCount(UserContext& context);
|
||||
void enqueGetFeedbacks(UserContext& context);
|
||||
std::size_t processGetFeedbacks(std::string_view body, UserContext& context);
|
||||
void tryImportFeedback(const Feedback& feedback, UserContext& context);
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
boost::asio::io_context::strand _strand {_ioContext};
|
||||
Database::Db& _db;
|
||||
boost::asio::steady_timer _syncTimer {_ioContext};
|
||||
Http::IClient& _client;
|
||||
|
||||
std::unordered_map<Database::UserId, UserContext> _userContexts;
|
||||
|
||||
const std::size_t _maxSyncFeedbackCount;
|
||||
const std::chrono::hours _syncFeedbacksPeriod;
|
||||
};
|
||||
} // Scrobbling::ListenBrainz
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/StarredArtist.hpp"
|
||||
#include "services/database/StarredRelease.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/http/IClient.hpp"
|
||||
@@ -28,16 +30,16 @@
|
||||
#include "utils/Service.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - "
|
||||
using namespace Database;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool
|
||||
canBeScrobbled(Database::Session& session, Database::TrackId trackId, std::chrono::seconds duration)
|
||||
canBeScrobbled(Session& session, TrackId trackId, std::chrono::seconds duration)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::find(session, trackId)};
|
||||
const Track::pointer track {Track::find(session, trackId)};
|
||||
if (!track)
|
||||
return false;
|
||||
|
||||
@@ -47,18 +49,40 @@ namespace
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
{
|
||||
// maybe in the future this will be supported by ListenBrainz so set it to PendingAdd
|
||||
starredObj.modify()->setScrobblingState(Database::ScrobblingState::PendingAdd);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
starredObj.remove();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db)
|
||||
Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Db& db)
|
||||
: _ioContext {ioContext}
|
||||
, _db {db}
|
||||
, _baseAPIUrl {Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")}
|
||||
, _client {Http::createClient(_ioContext, _baseAPIUrl)}
|
||||
, _listensSynchronizer {_ioContext, db, *_client}
|
||||
, _feedbacksSynchronizer {_ioContext, db, *_client}
|
||||
{
|
||||
LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl;
|
||||
LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl << "'";
|
||||
}
|
||||
|
||||
Scrobbler::~Scrobbler()
|
||||
@@ -87,5 +111,41 @@ namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
_listensSynchronizer.enqueListen(timedListen);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onStarred(StarredArtistId starredArtistId)
|
||||
{
|
||||
::onStarred<StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onUnstarred(StarredArtistId starredArtistId)
|
||||
{
|
||||
::onUnstarred<StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onStarred(StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onStarred<StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onUnstarred(StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onUnstarred<StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onStarred(StarredTrackId starredTrackId)
|
||||
{
|
||||
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Love, starredTrackId);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::onUnstarred(StarredTrackId starredtrackId)
|
||||
{
|
||||
_feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId);
|
||||
}
|
||||
} // namespace Scrobbling::ListenBrainz
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include "IScrobbler.hpp"
|
||||
#include "FeedbacksSynchronizer.hpp"
|
||||
#include "ListensSynchronizer.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
class TrackList;
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
@@ -50,11 +50,23 @@ namespace Scrobbling::ListenBrainz
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
// Submit listens
|
||||
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
|
||||
|
||||
// Star
|
||||
void onStarred(Database::StarredArtistId starredArtistId) override;
|
||||
void onUnstarred(Database::StarredArtistId starredArtistId) override;
|
||||
void onStarred(Database::StarredReleaseId starredReleaseId) override;
|
||||
void onUnstarred(Database::StarredReleaseId starredReleaseId) override;
|
||||
void onStarred(Database::StarredTrackId starredTrackId) override;
|
||||
void onUnstarred(Database::StarredTrackId starredTrackId) override;
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
Database::Db& _db;
|
||||
std::string _baseAPIUrl;
|
||||
std::unique_ptr<Http::IClient> _client;
|
||||
ListensSynchronizer _listensSynchronizer;
|
||||
FeedbacksSynchronizer _feedbacksSynchronizer;
|
||||
};
|
||||
} // Scrobbling::ListenBrainz
|
||||
|
||||
|
||||
@@ -31,19 +31,14 @@
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/scrobbling/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/http/IClient.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz Synchronizer] - "
|
||||
#define LOG_EX(sev) LMS_LOG_EX(Module::SCROBBLING, sev) << "[listenbrainz Synchronizer] - "
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace Scrobbling::ListenBrainz;
|
||||
@@ -128,29 +123,6 @@ namespace
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
parseValidateToken(std::string_view msgBody)
|
||||
{
|
||||
std::string listenBrainzUserName;
|
||||
|
||||
Wt::Json::ParseError error;
|
||||
Wt::Json::Object root;
|
||||
if (!Wt::Json::parse(std::string {msgBody}, root, error))
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
if (!root.get("valid").orIfNull(false))
|
||||
{
|
||||
LOG(INFO) << "Invalid listenbrainz user";
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
listenBrainzUserName = root.get("user_name").orIfNull("");
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
parseListenCount(std::string_view msgBody)
|
||||
{
|
||||
@@ -394,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<Database::Listen>(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();
|
||||
@@ -536,7 +508,7 @@ namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
_strand.dispatch([this, &context]
|
||||
{
|
||||
LOG_EX(context.importedListenCount > 0 ? Severity::INFO : Severity::DEBUG) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
||||
LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
||||
context.syncing = false;
|
||||
|
||||
if (!isSyncing())
|
||||
@@ -562,7 +534,7 @@ namespace Scrobbling::ListenBrainz
|
||||
request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} };
|
||||
request.onSuccessFunc = [this, &context] (std::string_view msgBody)
|
||||
{
|
||||
context.listenBrainzUserName = parseValidateToken(msgBody);
|
||||
context.listenBrainzUserName = Utils::parseValidateToken(msgBody);
|
||||
if (context.listenBrainzUserName.empty())
|
||||
{
|
||||
onSyncEnded(context);
|
||||
@@ -588,21 +560,24 @@ namespace Scrobbling::ListenBrainz
|
||||
request.priority = Http::ClientRequestParameters::Priority::Low;
|
||||
request.onSuccessFunc = [=, &context] (std::string_view msgBody)
|
||||
{
|
||||
const auto listenCount = parseListenCount(msgBody);
|
||||
if (listenCount)
|
||||
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
|
||||
|
||||
bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)};
|
||||
context.listenCount = listenCount;
|
||||
|
||||
if (!needSync)
|
||||
_strand.dispatch([=, &context]
|
||||
{
|
||||
onSyncEnded(context);
|
||||
return;
|
||||
}
|
||||
const auto listenCount = parseListenCount(msgBody);
|
||||
if (listenCount)
|
||||
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
|
||||
|
||||
context.maxDateTime = Wt::WDateTime::currentDateTime();
|
||||
enqueGetListens(context);
|
||||
bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)};
|
||||
context.listenCount = listenCount;
|
||||
|
||||
if (!needSync)
|
||||
{
|
||||
onSyncEnded(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.maxDateTime = Wt::WDateTime::currentDateTime();
|
||||
enqueGetListens(context);
|
||||
});
|
||||
};
|
||||
request.onFailureFunc = [this, &context]
|
||||
{
|
||||
|
||||
@@ -26,16 +26,13 @@
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/ListenId.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
#include "services/scrobbling/Listen.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Http
|
||||
@@ -68,9 +65,9 @@ namespace Scrobbling::ListenBrainz
|
||||
UserContext& operator=(const UserContext&) = delete;
|
||||
UserContext& operator=(UserContext&&) = delete;
|
||||
|
||||
const Database::UserId userId;
|
||||
bool syncing {};
|
||||
std::optional<std::size_t> listenCount {};
|
||||
const Database::UserId userId;
|
||||
bool syncing {};
|
||||
std::optional<std::size_t> listenCount {};
|
||||
|
||||
// resetted at each sync
|
||||
std::string listenBrainzUserName; // need to be resolved first
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Parser.h>
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
|
||||
@@ -35,4 +38,27 @@ namespace Scrobbling::ListenBrainz::Utils
|
||||
|
||||
return user->getListenBrainzToken();
|
||||
}
|
||||
|
||||
std::string
|
||||
parseValidateToken(std::string_view msgBody)
|
||||
{
|
||||
std::string listenBrainzUserName;
|
||||
|
||||
Wt::Json::ParseError error;
|
||||
Wt::Json::Object root;
|
||||
if (!Wt::Json::parse(std::string {msgBody}, root, error))
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
if (!root.get("valid").orIfNull(false))
|
||||
{
|
||||
LOG(INFO) << "Invalid listenbrainz user";
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
listenBrainzUserName = root.get("user_name").orIfNull("");
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@
|
||||
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - "
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||
|
||||
std::string parseValidateToken(std::string_view msgBody);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
|
||||
class Exception : public LmsException
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -422,7 +422,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const
|
||||
|
||||
static
|
||||
Response::Node
|
||||
artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, bool id3)
|
||||
artistToResponseNode(const Artist::pointer& artist, Session& session, const User::pointer& user, bool id3)
|
||||
{
|
||||
Response::Node artistNode;
|
||||
|
||||
@@ -430,7 +430,10 @@ artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, b
|
||||
artistNode.setAttribute("name", artist->getName());
|
||||
|
||||
if (id3)
|
||||
artistNode.setAttribute("albumCount", artist->getReleaseCount());
|
||||
{
|
||||
const auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist->getId()))};
|
||||
artistNode.setAttribute("albumCount", releases.results.size());
|
||||
}
|
||||
|
||||
if (Service<Scrobbling::IScrobblingService>::get()->isStarred(user->getId(), artist->getId()))
|
||||
artistNode.setAttribute("starred", reportedStarredDate);
|
||||
@@ -550,7 +553,7 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
tracklist = TrackList::find(context.dbSession, *id);
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError {};
|
||||
}
|
||||
@@ -560,7 +563,7 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
}
|
||||
else
|
||||
{
|
||||
tracklist = TrackList::create(context.dbSession, *name, TrackList::Type::Playlist, false, user);
|
||||
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, user);
|
||||
}
|
||||
|
||||
for (const TrackId trackId : trackIds)
|
||||
@@ -569,7 +572,7 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
TrackListEntry::create(context.dbSession, track, tracklist);
|
||||
context.dbSession.create<TrackListEntry>(track, tracklist);
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
@@ -591,7 +594,7 @@ handleCreateUserRequest(RequestContext& context)
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError {};
|
||||
|
||||
user = User::create(context.dbSession, username);
|
||||
user = context.dbSession.create<User>(username);
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
@@ -641,7 +644,7 @@ handleDeletePlaylistRequest(RequestContext& context)
|
||||
TrackList::pointer tracklist {TrackList::find(context.dbSession, id)};
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError {};
|
||||
}
|
||||
@@ -857,9 +860,12 @@ handleGetAlbumRequest(RequestContext& context)
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)};
|
||||
|
||||
auto tracks {release->getTracks()};
|
||||
for (const Track::pointer& track : tracks)
|
||||
const auto tracks {Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release))};
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
const Track::pointer track {Track::find(context.dbSession, trackId)};
|
||||
releaseNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
response.addNode("album", std::move(releaseNode));
|
||||
|
||||
@@ -884,9 +890,9 @@ handleGetArtistRequest(RequestContext& context)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
|
||||
Response::Node artistNode {artistToResponseNode(artist, context.dbSession, user, true /* id3 */)};
|
||||
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
const auto releases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(artist->getId()))};
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release {Release::find(context.dbSession, releaseId)};
|
||||
@@ -936,7 +942,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)};
|
||||
if (similarArtist)
|
||||
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3));
|
||||
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(similarArtist, context.dbSession, user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,7 +994,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
for (const ArtistId artistId : artistIds.results)
|
||||
{
|
||||
const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(artist, context.dbSession, user, false /* no id3 */));
|
||||
}
|
||||
}
|
||||
else if (artistId)
|
||||
@@ -1001,7 +1007,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
|
||||
|
||||
auto releases {artist->getReleases(Range {})};
|
||||
const auto releases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId))};
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release {Release::find(context.dbSession, releaseId)};
|
||||
@@ -1018,9 +1024,12 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName()));
|
||||
|
||||
auto tracks {release->getTracks()};
|
||||
for (const Track::pointer& track : tracks)
|
||||
const auto tracks {Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release))};
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
const Track::pointer track {Track::find(context.dbSession, trackId)};
|
||||
directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
}
|
||||
else
|
||||
throw BadParameterGenericError {"id"};
|
||||
@@ -1095,7 +1104,7 @@ handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
||||
indexNode.setAttribute("name", std::string {sortChar});
|
||||
|
||||
for (const Artist::pointer& artist :artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(artist, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -1160,26 +1169,34 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
// "Returns a random collection of songs from the given artist and similar artists"
|
||||
auto tracks {artist->getRandomTracks(count / 2)};
|
||||
const auto trackResults {Track::find(context.dbSession, Track::FindParameters {}
|
||||
.setArtist(artist->getId())
|
||||
.setRange({0, count / 2})
|
||||
.setSortMethod(TrackSortMethod::Random))};
|
||||
|
||||
std::vector<TrackId> tracks {trackResults.results};
|
||||
|
||||
for (const ArtistId similarArtistId : similarArtistIds)
|
||||
{
|
||||
const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)};
|
||||
if (!similarArtist)
|
||||
continue;
|
||||
|
||||
auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)};
|
||||
const auto similarArtistTracks {Track::find(context.dbSession, Track::FindParameters {}
|
||||
.setArtist(similarArtistId)
|
||||
.setRange({0, (count / 2) / 5})
|
||||
.setSortMethod(TrackSortMethod::Random))};
|
||||
|
||||
tracks.insert(std::end(tracks),
|
||||
std::make_move_iterator(std::begin(similarArtistTracks)),
|
||||
std::make_move_iterator(std::end(similarArtistTracks)));
|
||||
std::begin(similarArtistTracks.results),
|
||||
std::end(similarArtistTracks.results));
|
||||
}
|
||||
|
||||
Random::shuffleContainer(tracks);
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
|
||||
for (const Track::pointer& track : tracks)
|
||||
for (const TrackId trackId : tracks)
|
||||
{
|
||||
const Track::pointer track {Track::find(context.dbSession, trackId)};
|
||||
similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1216,7 +1233,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||
for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range {}).results)
|
||||
{
|
||||
if (auto artist {Artist::find(context.dbSession, artistId)})
|
||||
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
starredNode.addArrayChild("artist", artistToResponseNode(artist, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range {}).results)
|
||||
@@ -1304,7 +1321,11 @@ handleGetPlaylistsRequest(RequestContext& context)
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node& playlistsNode {response.createNode("playlists")};
|
||||
|
||||
auto tracklistIds {TrackList::find(context.dbSession, context.userId, TrackList::Type::Playlist, Range {})};
|
||||
TrackList::FindParameters params;
|
||||
params.setUser(context.userId);
|
||||
params.setType(TrackListType::Playlist);
|
||||
|
||||
auto tracklistIds {TrackList::find(context.dbSession, params)};
|
||||
for (const TrackListId trackListId : tracklistIds.results)
|
||||
{
|
||||
const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)};
|
||||
@@ -1433,7 +1454,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
for (const ArtistId artistId : artistIds.results)
|
||||
{
|
||||
const auto artist {Artist::find(context.dbSession, artistId)};
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(artist, context.dbSession, user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,7 +1669,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
TrackList::pointer tracklist {TrackList::find(context.dbSession, id)};
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackListType::Playlist)
|
||||
{
|
||||
throw RequestedDataNotFoundError {};
|
||||
}
|
||||
@@ -1678,7 +1699,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
TrackListEntry::create(context.dbSession, track, tracklist);
|
||||
context.dbSession.create<TrackListEntry>(track, tracklist);
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
@@ -1733,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<TrackBookmark>(user, track);
|
||||
|
||||
bookmark.modify()->setOffset(std::chrono::milliseconds {position});
|
||||
if (comment)
|
||||
@@ -1803,115 +1824,115 @@ struct RequestEntryPointInfo
|
||||
static const std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||
{
|
||||
// System
|
||||
{"ping", {handlePingRequest}},
|
||||
{"getLicense", {handleGetLicenseRequest}},
|
||||
{"/ping", {handlePingRequest}},
|
||||
{"/getLicense", {handleGetLicenseRequest}},
|
||||
|
||||
// Browsing
|
||||
{"getMusicFolders", {handleGetMusicFoldersRequest}},
|
||||
{"getIndexes", {handleGetIndexesRequest}},
|
||||
{"getMusicDirectory", {handleGetMusicDirectoryRequest}},
|
||||
{"getGenres", {handleGetGenresRequest}},
|
||||
{"getArtists", {handleGetArtistsRequest}},
|
||||
{"getArtist", {handleGetArtistRequest}},
|
||||
{"getAlbum", {handleGetAlbumRequest}},
|
||||
{"getSong", {handleNotImplemented}},
|
||||
{"getVideos", {handleNotImplemented}},
|
||||
{"getArtistInfo", {handleGetArtistInfoRequest}},
|
||||
{"getArtistInfo2", {handleGetArtistInfo2Request}},
|
||||
{"getAlbumInfo", {handleNotImplemented}},
|
||||
{"getAlbumInfo2", {handleNotImplemented}},
|
||||
{"getSimilarSongs", {handleGetSimilarSongsRequest}},
|
||||
{"getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
||||
{"getTopSongs", {handleNotImplemented}},
|
||||
{"/getMusicFolders", {handleGetMusicFoldersRequest}},
|
||||
{"/getIndexes", {handleGetIndexesRequest}},
|
||||
{"/getMusicDirectory", {handleGetMusicDirectoryRequest}},
|
||||
{"/getGenres", {handleGetGenresRequest}},
|
||||
{"/getArtists", {handleGetArtistsRequest}},
|
||||
{"/getArtist", {handleGetArtistRequest}},
|
||||
{"/getAlbum", {handleGetAlbumRequest}},
|
||||
{"/getSong", {handleNotImplemented}},
|
||||
{"/getVideos", {handleNotImplemented}},
|
||||
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
||||
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
||||
{"/getAlbumInfo", {handleNotImplemented}},
|
||||
{"/getAlbumInfo2", {handleNotImplemented}},
|
||||
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
||||
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
||||
{"/getTopSongs", {handleNotImplemented}},
|
||||
|
||||
// Album/song lists
|
||||
{"getAlbumList", {handleGetAlbumListRequest}},
|
||||
{"getAlbumList2", {handleGetAlbumList2Request}},
|
||||
{"getRandomSongs", {handleGetRandomSongsRequest}},
|
||||
{"getSongsByGenre", {handleGetSongsByGenreRequest}},
|
||||
{"getNowPlaying", {handleNotImplemented}},
|
||||
{"getStarred", {handleGetStarredRequest}},
|
||||
{"getStarred2", {handleGetStarred2Request}},
|
||||
{"/getAlbumList", {handleGetAlbumListRequest}},
|
||||
{"/getAlbumList2", {handleGetAlbumList2Request}},
|
||||
{"/getRandomSongs", {handleGetRandomSongsRequest}},
|
||||
{"/getSongsByGenre", {handleGetSongsByGenreRequest}},
|
||||
{"/getNowPlaying", {handleNotImplemented}},
|
||||
{"/getStarred", {handleGetStarredRequest}},
|
||||
{"/getStarred2", {handleGetStarred2Request}},
|
||||
|
||||
// Searching
|
||||
{"search", {handleNotImplemented}},
|
||||
{"search2", {handleSearch2Request}},
|
||||
{"search3", {handleSearch3Request}},
|
||||
{"/search", {handleNotImplemented}},
|
||||
{"/search2", {handleSearch2Request}},
|
||||
{"/search3", {handleSearch3Request}},
|
||||
|
||||
// Playlists
|
||||
{"getPlaylists", {handleGetPlaylistsRequest}},
|
||||
{"getPlaylist", {handleGetPlaylistRequest}},
|
||||
{"createPlaylist", {handleCreatePlaylistRequest}},
|
||||
{"updatePlaylist", {handleUpdatePlaylistRequest}},
|
||||
{"deletePlaylist", {handleDeletePlaylistRequest}},
|
||||
{"/getPlaylists", {handleGetPlaylistsRequest}},
|
||||
{"/getPlaylist", {handleGetPlaylistRequest}},
|
||||
{"/createPlaylist", {handleCreatePlaylistRequest}},
|
||||
{"/updatePlaylist", {handleUpdatePlaylistRequest}},
|
||||
{"/deletePlaylist", {handleDeletePlaylistRequest}},
|
||||
|
||||
// Media retrieval
|
||||
{"hls", {handleNotImplemented}},
|
||||
{"getCaptions", {handleNotImplemented}},
|
||||
{"getLyrics", {handleNotImplemented}},
|
||||
{"getAvatar", {handleNotImplemented}},
|
||||
{"/hls", {handleNotImplemented}},
|
||||
{"/getCaptions", {handleNotImplemented}},
|
||||
{"/getLyrics", {handleNotImplemented}},
|
||||
{"/getAvatar", {handleNotImplemented}},
|
||||
|
||||
// Media annotation
|
||||
{"star", {handleStarRequest}},
|
||||
{"unstar", {handleUnstarRequest}},
|
||||
{"setRating", {handleNotImplemented}},
|
||||
{"scrobble", {handleScrobble}},
|
||||
{"/star", {handleStarRequest}},
|
||||
{"/unstar", {handleUnstarRequest}},
|
||||
{"/setRating", {handleNotImplemented}},
|
||||
{"/scrobble", {handleScrobble}},
|
||||
|
||||
// Sharing
|
||||
{"getShares", {handleNotImplemented}},
|
||||
{"createShares", {handleNotImplemented}},
|
||||
{"updateShare", {handleNotImplemented}},
|
||||
{"deleteShare", {handleNotImplemented}},
|
||||
{"/getShares", {handleNotImplemented}},
|
||||
{"/createShares", {handleNotImplemented}},
|
||||
{"/updateShare", {handleNotImplemented}},
|
||||
{"/deleteShare", {handleNotImplemented}},
|
||||
|
||||
// Podcast
|
||||
{"getPodcasts", {handleNotImplemented}},
|
||||
{"getNewestPodcasts", {handleNotImplemented}},
|
||||
{"refreshPodcasts", {handleNotImplemented}},
|
||||
{"createPodcastChannel", {handleNotImplemented}},
|
||||
{"deletePodcastChannel", {handleNotImplemented}},
|
||||
{"deletePodcastEpisode", {handleNotImplemented}},
|
||||
{"downloadPodcastEpisode", {handleNotImplemented}},
|
||||
{"/getPodcasts", {handleNotImplemented}},
|
||||
{"/getNewestPodcasts", {handleNotImplemented}},
|
||||
{"/refreshPodcasts", {handleNotImplemented}},
|
||||
{"/createPodcastChannel", {handleNotImplemented}},
|
||||
{"/deletePodcastChannel", {handleNotImplemented}},
|
||||
{"/deletePodcastEpisode", {handleNotImplemented}},
|
||||
{"/downloadPodcastEpisode", {handleNotImplemented}},
|
||||
|
||||
// Jukebox
|
||||
{"jukeboxControl", {handleNotImplemented}},
|
||||
{"/jukeboxControl", {handleNotImplemented}},
|
||||
|
||||
// Internet radio
|
||||
{"getInternetRadioStations", {handleNotImplemented}},
|
||||
{"createInternetRadioStation", {handleNotImplemented}},
|
||||
{"updateInternetRadioStation", {handleNotImplemented}},
|
||||
{"deleteInternetRadioStation", {handleNotImplemented}},
|
||||
{"/getInternetRadioStations", {handleNotImplemented}},
|
||||
{"/createInternetRadioStation", {handleNotImplemented}},
|
||||
{"/updateInternetRadioStation", {handleNotImplemented}},
|
||||
{"/deleteInternetRadioStation", {handleNotImplemented}},
|
||||
|
||||
// Chat
|
||||
{"getChatMessages", {handleNotImplemented}},
|
||||
{"addChatMessages", {handleNotImplemented}},
|
||||
{"/getChatMessages", {handleNotImplemented}},
|
||||
{"/addChatMessages", {handleNotImplemented}},
|
||||
|
||||
// User management
|
||||
{"getUser", {handleGetUserRequest}},
|
||||
{"getUsers", {handleGetUsersRequest, {UserType::ADMIN}}},
|
||||
{"createUser", {handleCreateUserRequest, {UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
{"updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}},
|
||||
{"deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}},
|
||||
{"changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
{"/getUser", {handleGetUserRequest}},
|
||||
{"/getUsers", {handleGetUsersRequest, {UserType::ADMIN}}},
|
||||
{"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
{"/updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}},
|
||||
{"/deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}},
|
||||
{"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
|
||||
// Bookmarks
|
||||
{"getBookmarks", {handleGetBookmarks}},
|
||||
{"createBookmark", {handleCreateBookmark}},
|
||||
{"deleteBookmark", {handleDeleteBookmark}},
|
||||
{"getPlayQueue", {handleNotImplemented}},
|
||||
{"savePlayQueue", {handleNotImplemented}},
|
||||
{"/getBookmarks", {handleGetBookmarks}},
|
||||
{"/createBookmark", {handleCreateBookmark}},
|
||||
{"/deleteBookmark", {handleDeleteBookmark}},
|
||||
{"/getPlayQueue", {handleNotImplemented}},
|
||||
{"/savePlayQueue", {handleNotImplemented}},
|
||||
|
||||
// Media library scanning
|
||||
{"getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}},
|
||||
{"startScan", {Scan::handleStartScan, {UserType::ADMIN}}},
|
||||
{"/getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}},
|
||||
{"/startScan", {Scan::handleStartScan, {UserType::ADMIN}}},
|
||||
};
|
||||
|
||||
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
|
||||
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
||||
{
|
||||
// Media retrieval
|
||||
{"download", Stream::handleDownload},
|
||||
{"stream", Stream::handleStream},
|
||||
{"getCoverArt", handleGetCoverArt},
|
||||
{"/download", Stream::handleDownload},
|
||||
{"/stream", Stream::handleStream},
|
||||
{"/getCoverArt", handleGetCoverArt},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ bufferToString(const std::vector<unsigned char>& data)
|
||||
}
|
||||
|
||||
std::string
|
||||
replaceInString(const std::string& str, const std::string& from, const std::string& to)
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
std::string res {str};
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -87,7 +87,7 @@ readAs(std::string_view str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
replaceInString(const std::string& str, const std::string& from, const std::string& to);
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
|
||||
@@ -39,6 +39,8 @@ 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
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
|
||||
+1
-1
@@ -275,7 +275,7 @@ int main(int argc, char* argv[])
|
||||
if (config->getBool("api-subsonic", true))
|
||||
{
|
||||
subsonicResource = API::Subsonic::createSubsonicResource(database);
|
||||
server.addResource(subsonicResource.get(), "rest/");
|
||||
server.addResource(subsonicResource.get(), "/rest");
|
||||
}
|
||||
|
||||
// bind UI entry point
|
||||
|
||||
@@ -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"
|
||||
@@ -44,6 +45,7 @@
|
||||
#include "admin/DatabaseSettingsView.hpp"
|
||||
#include "admin/UserView.hpp"
|
||||
#include "admin/UsersView.hpp"
|
||||
#include "common/Template.hpp"
|
||||
#include "explore/Explore.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
@@ -190,6 +192,8 @@ 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");
|
||||
|
||||
// Handle Media Scanner events and other session events
|
||||
@@ -298,6 +302,23 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
LmsApplication::createTrackListAnchor(Database::TrackList::pointer trackList, bool addText)
|
||||
{
|
||||
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
|
||||
auto res {std::make_unique<Wt::WAnchor>(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)
|
||||
{
|
||||
@@ -347,6 +368,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
{ "/release", IdxExplore, false },
|
||||
{ "/search", IdxExplore, false },
|
||||
{ "/tracks", IdxExplore, false },
|
||||
{ "/tracklists", IdxExplore, false },
|
||||
{ "/tracklist", IdxExplore, false },
|
||||
{ "/playqueue", IdxPlayQueue, false },
|
||||
{ "/settings", IdxSettings, false },
|
||||
{ "/admin/database", IdxAdminDatabase, true },
|
||||
@@ -418,7 +441,7 @@ LmsApplication::createHome()
|
||||
|
||||
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WTemplate* navbar {main->bindNew<Wt::WTemplate>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
|
||||
Template* navbar {main->bindNew<Template>("navbar", Wt::WString::tr("Lms.main.template.navbar"))};
|
||||
navbar->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_notificationContainer = main->bindNew<NotificationContainer>("notifications");
|
||||
@@ -431,9 +454,9 @@ LmsApplication::createHome()
|
||||
navbar->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
||||
navbar->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
||||
navbar->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
||||
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink {Wt::LinkType::InternalPath, "/tracklists"}, Wt::WString::tr("Lms.Explore.tracklists"));
|
||||
|
||||
Filters* filters {navbar->bindNew<Filters>("filters")};
|
||||
navbar->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||
|
||||
@@ -481,6 +504,7 @@ LmsApplication::createHome()
|
||||
mainStack->addNew<UserView>();
|
||||
}
|
||||
|
||||
explore->setMaxTrackCountForAction(_playQueue->getCapacity());
|
||||
explore->tracksAction.connect([this] (PlayQueueAction action, const std::vector<Database::TrackId>& trackIds)
|
||||
{
|
||||
_playQueue->processTracks(action, trackIds);
|
||||
@@ -542,7 +566,7 @@ LmsApplication::createHome()
|
||||
});
|
||||
}
|
||||
|
||||
internalPathChanged().connect([=]
|
||||
internalPathChanged().connect(mainStack, [=]
|
||||
{
|
||||
handlePathChange(*mainStack, isAdmin);
|
||||
});
|
||||
|
||||
@@ -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<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
|
||||
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
|
||||
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool addText = true);
|
||||
static std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::ObjectPtr<Database::TrackList> trackList, bool addText = true);
|
||||
|
||||
MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
|
||||
PlayQueue& getPlayQueue() const { return *_playQueue; }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
#include <Wt/Json/Serializer.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
@@ -209,6 +210,12 @@ MediaPlayer::MediaPlayer()
|
||||
_artist = bindNew<Wt::WAnchor>("artist");
|
||||
_release = bindNew<Wt::WAnchor>("release");
|
||||
|
||||
{
|
||||
Wt::WPushButton* playQueueBtn {bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn"), Wt::TextFormat::XHTML)};
|
||||
playQueueBtn->setLink(Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"});
|
||||
playQueueBtn->setToolTip(tr("Lms.PlayQueue.playqueue"));
|
||||
}
|
||||
|
||||
_settingsLoaded.connect([this](const std::string& settings)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
|
||||
|
||||
+226
-16
@@ -20,7 +20,13 @@
|
||||
#include "PlayQueue.hpp"
|
||||
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WFormModel.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WRadioButton.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
@@ -36,25 +42,97 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
namespace
|
||||
{
|
||||
class CreateTrackListModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
static inline const Field NameField {"name"};
|
||||
|
||||
CreateTrackListModel()
|
||||
{
|
||||
addField(NameField);
|
||||
setValidator(NameField, createMandatoryValidator());
|
||||
}
|
||||
|
||||
Wt::WString getName() const { return valueText(NameField); }
|
||||
};
|
||||
|
||||
class ReplaceTrackListModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
static inline const Field NameField {"name"};
|
||||
using TrackListModel = ValueStringModel<Database::TrackListId>;
|
||||
|
||||
ReplaceTrackListModel()
|
||||
{
|
||||
addField(NameField);
|
||||
setValidator(NameField, createMandatoryValidator());
|
||||
}
|
||||
|
||||
Database::TrackListId getTrackListId() const
|
||||
{
|
||||
auto row {trackListModel->getRowFromString(valueText(NameField))};
|
||||
return trackListModel->getValue(*row);
|
||||
}
|
||||
|
||||
static
|
||||
std::shared_ptr<TrackListModel>
|
||||
createTrackListModel()
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
auto model {std::make_shared<TrackListModel>()};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
TrackList::FindParameters params;
|
||||
params.setType(TrackListType::Playlist);
|
||||
params.setUser(LmsApp->getUserId());
|
||||
params.setSortMethod(TrackListSortMethod::Name);
|
||||
|
||||
auto tracklists {TrackList::find(LmsApp->getDbSession(), params)};
|
||||
for (const TrackListId trackListId : tracklists.results)
|
||||
{
|
||||
const TrackList::pointer trackList {TrackList::find(LmsApp->getDbSession(), trackListId)};
|
||||
model->add(Wt::WString::fromUTF8(std::string {trackList->getName()}), trackListId);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
std::shared_ptr<TrackListModel> trackListModel {createTrackListModel()};
|
||||
};
|
||||
}
|
||||
|
||||
PlayQueue::PlayQueue()
|
||||
: Template {Wt::WString::tr("Lms.PlayQueue.template")}
|
||||
{
|
||||
addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WText* clearBtn = bindNew<Wt::WText>("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WPushButton* clearBtn {bindNew<Wt::WPushButton>("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML)};
|
||||
clearBtn->clicked().connect([=]
|
||||
{
|
||||
clearTracks();
|
||||
});
|
||||
|
||||
Wt::WPushButton* saveBtn {bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr("Lms.PlayQueue.template.save-btn"), Wt::TextFormat::XHTML)};
|
||||
saveBtn->clicked().connect([=]
|
||||
{
|
||||
saveAsTrackList();
|
||||
});
|
||||
|
||||
_entriesContainer = bindNew<InfiniteScrollingContainer>("entries");
|
||||
_entriesContainer->onRequestElements.connect([this]
|
||||
{
|
||||
@@ -62,7 +140,7 @@ PlayQueue::PlayQueue()
|
||||
updateCurrentTrack(true);
|
||||
});
|
||||
|
||||
Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WPushButton* shuffleBtn {bindNew<Wt::WPushButton>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML)};
|
||||
shuffleBtn->clicked().connect([=]
|
||||
{
|
||||
{
|
||||
@@ -74,7 +152,7 @@ PlayQueue::PlayQueue()
|
||||
|
||||
getTrackList().modify()->clear();
|
||||
for (const auto& entry : entries)
|
||||
Database::TrackListEntry::create(LmsApp->getDbSession(), entry->getTrack(), trackList);
|
||||
LmsApp->getDbSession().create<Database::TrackListEntry>(entry->getTrack(), trackList);
|
||||
}
|
||||
_entriesContainer->clear();
|
||||
addSome();
|
||||
@@ -108,7 +186,8 @@ PlayQueue::PlayQueue()
|
||||
_radioBtn->setCheckState(Wt::CheckState::Checked);
|
||||
}
|
||||
|
||||
_nbTracks = bindNew<Wt::WText>("nb-tracks");
|
||||
_nbTracks = bindNew<Wt::WText>("track-count");
|
||||
_duration = bindNew<Wt::WText>("duration");
|
||||
|
||||
LmsApp->preQuit().connect([=]
|
||||
{
|
||||
@@ -148,14 +227,14 @@ PlayQueue::PlayQueue()
|
||||
});
|
||||
|
||||
static const std::string queuedListName {"__queued_tracks__"};
|
||||
trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, LmsApp->getUserId());
|
||||
trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackListType::Internal, LmsApp->getUserId());
|
||||
if (!trackList)
|
||||
trackList = Database::TrackList::create(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, false, LmsApp->getUser());
|
||||
trackList = LmsApp->getDbSession().create<Database::TrackList>(queuedListName, Database::TrackListType::Internal, false, LmsApp->getUser());
|
||||
}
|
||||
else
|
||||
{
|
||||
static const std::string currentPlayQueueName {"__current__playqueue__"};
|
||||
trackList = Database::TrackList::create(LmsApp->getDbSession(), currentPlayQueueName, Database::TrackList::Type::Internal, false, LmsApp->getUser());
|
||||
trackList = LmsApp->getDbSession().create<Database::TrackList>(currentPlayQueueName, Database::TrackListType::Internal, false, LmsApp->getUser());
|
||||
}
|
||||
|
||||
_tracklistId = trackList->getId();
|
||||
@@ -187,7 +266,7 @@ bool
|
||||
PlayQueue::isFull() const
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
return getTrackList()->getCount() == _nbMaxEntries;
|
||||
return getTrackList()->getCount() == getCapacity();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -287,7 +366,9 @@ PlayQueue::updateInfo()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
_nbTracks->setText(Wt::WString::tr("Lms.PlayQueue.nb-tracks").arg(static_cast<unsigned>(getTrackList()->getCount())));
|
||||
const auto trackCount {getTrackList()->getCount()};
|
||||
_nbTracks->setText(Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
|
||||
_duration->setText(Utils::durationToString(getTrackList()->getDuration()));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -313,7 +394,7 @@ PlayQueue::enqueueTracks(const std::vector<Database::TrackId>& trackIds)
|
||||
|
||||
auto tracklist {getTrackList()};
|
||||
|
||||
std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()};
|
||||
std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > getCapacity() ? getCapacity() - tracklist->getCount() : trackIds.size()};
|
||||
for (const Database::TrackId trackId : trackIds)
|
||||
{
|
||||
Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)};
|
||||
@@ -323,7 +404,7 @@ PlayQueue::enqueueTracks(const std::vector<Database::TrackId>& trackIds)
|
||||
if (nbTracksQueued == nbTracksToEnqueue)
|
||||
break;
|
||||
|
||||
Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist);
|
||||
LmsApp->getDbSession().create<Database::TrackListEntry>(track, tracklist);
|
||||
nbTracksQueued++;
|
||||
}
|
||||
}
|
||||
@@ -368,10 +449,10 @@ PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::Tra
|
||||
}
|
||||
|
||||
if (nbAddedTracks > 0)
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
|
||||
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::seconds {2});
|
||||
|
||||
if (isFull())
|
||||
LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
|
||||
LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue"), Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::seconds {2});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -420,7 +501,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
|
||||
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
const std::optional<std::size_t> pos {_entriesContainer->getIndexOf(*entry)};
|
||||
@@ -428,7 +509,8 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
loadTrack(*pos, true);
|
||||
});
|
||||
|
||||
Wt::WText* delBtn {entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.template.delete-btn"), Wt::TextFormat::XHTML)};
|
||||
Wt::WPushButton* delBtn {entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.template.delete-btn"), Wt::TextFormat::XHTML)};
|
||||
delBtn->setToolTip(Wt::WString::tr("Lms.delete"));
|
||||
delBtn->clicked().connect([=]
|
||||
{
|
||||
// Remove the entry n both the widget tree and the playqueue
|
||||
@@ -451,7 +533,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML);
|
||||
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
|
||||
|
||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||
|
||||
@@ -540,5 +622,133 @@ PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track)
|
||||
return settings->replayGain.preAmpGainIfNoInfo;
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::saveAsTrackList()
|
||||
{
|
||||
auto modal {std::make_unique<Template>(Wt::WString::tr("Lms.PlayQueue.template.save-as-tracklist"))};
|
||||
modal->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
Wt::WWidget* modalPtr {modal.get()};
|
||||
|
||||
auto* cancelBtn {modal->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"))};
|
||||
cancelBtn->clicked().connect([=]
|
||||
{
|
||||
LmsApp->getModalManager().dispose(modalPtr);
|
||||
});
|
||||
|
||||
Wt::WStackedWidget* contentStack {modal->bindNew<Wt::WStackedWidget>("contents")};
|
||||
|
||||
// Create/Replace selector
|
||||
enum Index : int
|
||||
{
|
||||
CreateTrackList = 0,
|
||||
ReplaceTrackList = 1,
|
||||
};
|
||||
{
|
||||
modal->bindNew<Wt::WRadioButton>("replace-tracklist-btn")
|
||||
->clicked().connect([=]
|
||||
{
|
||||
contentStack->setCurrentIndex(Index::ReplaceTrackList);
|
||||
});
|
||||
|
||||
Wt::WRadioButton* createTrackListBtn {modal->bindNew<Wt::WRadioButton>("create-tracklist-btn")};
|
||||
createTrackListBtn->setChecked();
|
||||
createTrackListBtn->clicked().connect([=]
|
||||
{
|
||||
contentStack->setCurrentIndex(Index::CreateTrackList);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Create TrackList
|
||||
Wt::WTemplateFormView* createTrackList {contentStack->addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.PlayQueue.template.save-as-tracklist.create-tracklist"))};
|
||||
auto createTrackListModel {std::make_shared<CreateTrackListModel>()};
|
||||
createTrackList->setFormWidget(CreateTrackListModel::NameField, std::make_unique<Wt::WLineEdit>());
|
||||
createTrackList->updateView(createTrackListModel.get());
|
||||
|
||||
// Replace TrackList
|
||||
Wt::WTemplateFormView* replaceTrackList {contentStack->addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.PlayQueue.template.save-as-tracklist.replace-tracklist"))};
|
||||
auto replaceTrackListModel {std::make_shared<ReplaceTrackListModel>()};
|
||||
{
|
||||
auto name {std::make_unique<Wt::WComboBox>()};
|
||||
name->setModel(replaceTrackListModel->trackListModel);
|
||||
replaceTrackList->setFormWidget(ReplaceTrackListModel::NameField, std::move(name));
|
||||
}
|
||||
replaceTrackList->updateView(replaceTrackListModel.get());
|
||||
|
||||
auto* saveBtn {modal->bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr("Lms.save"))};
|
||||
saveBtn->clicked().connect([=]
|
||||
{
|
||||
bool success {};
|
||||
switch (contentStack->currentIndex())
|
||||
{
|
||||
case Index::CreateTrackList:
|
||||
createTrackList->updateModel(createTrackListModel.get());
|
||||
if (createTrackListModel->validate())
|
||||
{
|
||||
exportToNewTrackList(createTrackListModel->getName());
|
||||
success = true;
|
||||
}
|
||||
createTrackList->updateView(createTrackListModel.get());
|
||||
break;
|
||||
|
||||
case Index::ReplaceTrackList:
|
||||
replaceTrackList->updateModel(replaceTrackListModel.get());
|
||||
if (replaceTrackListModel->validate())
|
||||
{
|
||||
exportToTrackList(replaceTrackListModel->getTrackListId());
|
||||
success = true;
|
||||
}
|
||||
replaceTrackList->updateView(replaceTrackListModel.get());
|
||||
break;
|
||||
}
|
||||
|
||||
if (success)
|
||||
LmsApp->getModalManager().dispose(modalPtr);
|
||||
});
|
||||
|
||||
LmsApp->getModalManager().show(std::move(modal));
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::exportToNewTrackList(const Wt::WString& name)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Database::TrackListId trackListId;
|
||||
|
||||
{
|
||||
Session& session {LmsApp->getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
TrackList::pointer trackList {session.create<TrackList>(name.toUTF8(), TrackListType::Playlist, false, LmsApp->getUser())};
|
||||
trackListId = trackList->getId();
|
||||
}
|
||||
|
||||
exportToTrackList(trackListId);
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::exportToTrackList(Database::TrackListId trackListId)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Session& session {LmsApp->getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackList::pointer trackList {TrackList::find(LmsApp->getDbSession(), trackListId)};
|
||||
trackList.modify()->clear();
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setTrackList(_tracklistId);
|
||||
params.setDistinct(false);
|
||||
params.setSortMethod(TrackSortMethod::TrackList);
|
||||
|
||||
const auto tracks {Track::find(session, params)};
|
||||
|
||||
for (const TrackId trackId : tracks.results)
|
||||
session.create<TrackListEntry>(Track::find(session, trackId), trackList);
|
||||
}
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ class PlayQueue : public Template
|
||||
// Signal emitted when track is unselected (has to be stopped)
|
||||
Wt::Signal<> trackUnselected;
|
||||
|
||||
constexpr std::size_t getCapacity() const { return _capacity; }
|
||||
|
||||
private:
|
||||
Database::ObjectPtr<Database::TrackList> getTrackList() const;
|
||||
bool isFull() const;
|
||||
@@ -87,14 +89,19 @@ class PlayQueue : public Template
|
||||
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
|
||||
void addRadioTrackFromClusters();
|
||||
std::optional<float> getReplayGain(std::size_t pos, const Database::ObjectPtr<Database::Track>& track) const;
|
||||
void saveAsTrackList();
|
||||
|
||||
static inline constexpr std::size_t _nbMaxEntries {1000};
|
||||
void exportToNewTrackList(const Wt::WString& name);
|
||||
void exportToTrackList(Database::TrackListId trackList);
|
||||
|
||||
static inline constexpr std::size_t _capacity {1000};
|
||||
static inline constexpr std::size_t _batchSize {12};
|
||||
|
||||
bool _mediaPlayerSettingsLoaded {};
|
||||
Database::TrackListId _tracklistId {};
|
||||
InfiniteScrollingContainer* _entriesContainer {};
|
||||
Wt::WText* _nbTracks {};
|
||||
Wt::WText* _duration {};
|
||||
Wt::WCheckBox* _repeatBtn {};
|
||||
Wt::WCheckBox* _radioBtn {};
|
||||
std::optional<std::size_t> _trackPos; // current track position, if set
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/TrackListId.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
@@ -38,5 +39,6 @@ namespace UserInterface
|
||||
using PlayQueueActionArtistSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ArtistId>&>;
|
||||
using PlayQueueActionReleaseSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ReleaseId>&>;
|
||||
using PlayQueueActionTrackSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::TrackId>&>;
|
||||
using PlayQueueActionTrackListSignal = Wt::Signal<PlayQueueAction, Database::TrackListId>;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user