Merge branch 'playlists' into develop, fixes #57

This commit is contained in:
emeric
2022-07-03 14:18:17 +02:00
91 changed files with 1696 additions and 481 deletions
+1 -11
View File
@@ -15,15 +15,9 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* [ListenBrainz](https://listenbrainz.org) support for: * [ListenBrainz](https://listenbrainz.org) support for:
* Scrobbling and synchronizing listens * Scrobbling and synchronizing listens
* Synchronizing 'love' feedbacks * Synchronizing 'love' feedbacks
* Compilation support
* Disc subtitles support
* ReplayGain support * ReplayGain support
* Persistent play queue across sessions
* _Systemd_ integration
* User management, with several authentication backends, see [Deployment](INSTALL.md#deployment) * User management, with several authentication backends, see [Deployment](INSTALL.md#deployment)
* Subsonic API, with the following additional features: * Subsonic API
* Playlists
* Bookmarks
## Music discovery ## Music discovery
_LMS_ provides several ways to help you find the music you like: _LMS_ provides several ways to help you find the music you like:
@@ -51,9 +45,7 @@ __Notes on the self-organizing map__:
## Subsonic API ## Subsonic API
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_ and _DSub_. 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 browse 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. 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. __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.
@@ -86,11 +78,9 @@ _Wt_ (the web framework used) has some [built-in security measures](https://www.
* all the resources relative to the music collection (tracks, covers, etc.) are private to an anthenticated session * all the resources relative to the music collection (tracks, covers, etc.) are private to an anthenticated session
## Installation ## Installation
See [INSTALL.md](INSTALL.md) file. See [INSTALL.md](INSTALL.md) file.
## Contributing ## Contributing
Any feedback is welcome: Any feedback is welcome:
* feel free to participate in [discussions](https://github.com/epoupon/lms/discussions) if you have questions, * 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), * report any bug or request for new features in the [issue tracker](https://github.com/epoupon/lms/issues),
+5 -5
View File
@@ -14,9 +14,12 @@
<div class="p-2 flex-fill"> <div class="p-2 flex-fill">
${name}${<if-tag>}<span class="badge bg-info ms-2">${tag}</span>${</if-tag>} ${name}${<if-tag>}<span class="badge bg-info ms-2">${tag}</span>${</if-tag>}
</div> </div>
${<if-edit>}
<div class="p-2"> <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> </div>
${</if-edit>}
</div> </div>
</message> </message>
@@ -25,12 +28,9 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body">
${tr:Lms.Admin.Users.del-user-name}
</div>
<div class="modal-footer"> <div class="modal-footer">
${del-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"} ${del-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"}
</div> </div>
+4 -4
View File
@@ -10,13 +10,13 @@
${play-btn class="btn btn-primary"} ${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"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-shuffled class="btn dropdown-item"}</li> <li>${play-shuffled class="dropdown-item"}</li>
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${star class="btn dropdown-item"}</li> <li>${star class="dropdown-item"}</li>
${<if-has-mbid>} ${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li> <li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
${</if-has-mbid>} ${</if-has-mbid>}
<li>${download class="btn dropdown-item"}</li> <li>${download class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
+3 -6
View File
@@ -5,9 +5,6 @@
${contents} ${contents}
</message> </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"> <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"} ${add-filter class="btn btn-sm btn-outline-primary me-2"}${clusters class="d-inline-block align-items-center"}
</message> </message>
@@ -35,9 +32,9 @@
${value class="form-control"} ${value class="form-control"}
</div> </div>
</form> </form>
<div class="modal-footer"> </div>
${add-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"} <div class="modal-footer">
</div> ${add-btn class="btn btn-primary me-1"}${cancel-btn class="btn btn-secondary"}
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -32,7 +32,7 @@
${tracks class="nav-link"} ${tracks class="nav-link"}
</li> </li>
<li class="nav-item"> <li class="nav-item">
${playqueue class="nav-link"} ${tracklists class="nav-link"}
</li> </li>
<li class="nav-item"> <li class="nav-item">
${filters class="nav-link d-flex align-items-center"} ${filters class="nav-link d-flex align-items-center"}
+10 -5
View File
@@ -14,7 +14,7 @@
</div> </div>
<div class="d-flex align-items-center"> <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"> <button id="lms-mp-previous" class="btn btn-sm btn-outline-primary border-0">
<i class="fa fa-fw fa-step-backward"/> <i class="fa fa-fw fa-step-backward"/>
</button> </button>
@@ -29,16 +29,21 @@
${title class="d-block text-truncate"} ${title class="d-block text-truncate"}
${release class="text-decoration-none text-truncate link-success"} ${release class="text-decoration-none text-truncate link-success"}
</div> </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> <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"/> <input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8"/>
</div> </div>
<div class="d-none d-sm-block text-muted text-nowrap"> <div class="py-2 ps-2">
<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> ${playqueue-btn class="btn btn-outline-primary border-0"}
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
</div> </div>
</div> </div>
</div> </div>
</message> </message>
<message id="Lms.MediaPlayer.template.playqueue-btn"><i class="fa fa-fw fa-th-list"/></message>
</messages> </messages>
+16 -6
View File
@@ -6,9 +6,11 @@
<message id="Lms.add">Add</message> <message id="Lms.add">Add</message>
<message id="Lms.administration">Administration</message> <message id="Lms.administration">Administration</message>
<message id="Lms.apply">Apply</message> <message id="Lms.apply">Apply</message>
<message id="Lms.discard">Discard</message>
<message id="Lms.cancel">Cancel</message> <message id="Lms.cancel">Cancel</message>
<message id="Lms.create">Create</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.loading">Loading...</message>
<message id="Lms.login">Login</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> <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.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.quit-other-session">Another session has been open. Reopen this one?</message>
<message id="Lms.save">Save</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> <message id="Lms.user">User</message>
<!--Errors--> <!--Errors-->
@@ -31,6 +37,7 @@
<message id="Lms.Error.error-occurred">Error occured!</message> <message id="Lms.Error.error-occurred">Error occured!</message>
<message id="Lms.Error.go-home">Go home</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.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-allowed">You are not allowed to perform this operation</message>
<message id="Lms.Error.user-not-found">User not found</message> <message id="Lms.Error.user-not-found">User not found</message>
@@ -85,11 +92,8 @@
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">New user</message> <message id="Lms.Admin.Users.add">New user</message>
<message id="Lms.Admin.Users.admin">Admin</message> <message id="Lms.Admin.Users.admin">Admin</message>
<message id="Lms.Admin.Users.del">Delete</message> <message id="Lms.Admin.Users.del-user-confirm">Delete user?</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.demo">Demo</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.users">Users</message>
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> 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.play-shuffled">Play shuffled</message>
<message id="Lms.Explore.random">Random</message> <message id="Lms.Explore.random">Random</message>
<message id="Lms.Explore.recently-added">Recently added</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.recently-played">Recently played</message>
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.star">Star</message> <message id="Lms.Explore.star">Star</message>
<message id="Lms.Explore.starred">Starred</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.tracks">Tracks</message>
<message id="Lms.Explore.type">Type</message> <message id="Lms.Explore.type">Type</message>
<message id="Lms.Explore.unstar">Unstar</message> <message id="Lms.Explore.unstar">Unstar</message>
@@ -155,6 +161,9 @@
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</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--> <!--Explore:Search-->
<message id="Lms.Explore.Search.search-placeholder">Search...</message> <message id="Lms.Explore.Search.search-placeholder">Search...</message>
@@ -163,7 +172,7 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Clear</message> <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"> <message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">Added {1} track</plural> <plural case="0">Added {1} track</plural>
<plural case="1">Added {1} tracks</plural> <plural case="1">Added {1} tracks</plural>
@@ -172,6 +181,7 @@
<message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message> <message id="Lms.PlayQueue.playqueue-full">Play Queue full!</message>
<message id="Lms.PlayQueue.radio-mode">Radio mode</message> <message id="Lms.PlayQueue.radio-mode">Radio mode</message>
<message id="Lms.PlayQueue.repeat">Repeat</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> <message id="Lms.PlayQueue.shuffle">Shuffle</message>
<!--PlayHistory--> <!--PlayHistory-->
+16 -7
View File
@@ -6,9 +6,11 @@
<message id="Lms.add">Ajouter</message> <message id="Lms.add">Ajouter</message>
<message id="Lms.administration">Administration</message> <message id="Lms.administration">Administration</message>
<message id="Lms.apply">Appliquer</message> <message id="Lms.apply">Appliquer</message>
<message id="Lms.discard">Annuler</message>
<message id="Lms.cancel">Annuler</message> <message id="Lms.cancel">Annuler</message>
<message id="Lms.create">Créer</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.loading">Chargement...</message>
<message id="Lms.login">Login</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> <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.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.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
<message id="Lms.save">Sauvegarder</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> <message id="Lms.user">Utilisateur</message>
<!--Errors--> <!--Errors-->
@@ -31,6 +37,7 @@
<message id="Lms.Error.error-occurred">Une erreur est survenue!</message> <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.go-home">Retour à l'accueil</message>
<message id="Lms.Error.release-not-found">Cet album n'existe pas</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-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> <message id="Lms.Error.user-not-found">L'utilisateur n'existe pas</message>
@@ -85,11 +92,8 @@
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">Ajouter</message> <message id="Lms.Admin.Users.add">Ajouter</message>
<message id="Lms.Admin.Users.admin">Admin</message> <message id="Lms.Admin.Users.admin">Admin</message>
<message id="Lms.Admin.Users.del">Supprimer</message> <message id="Lms.Admin.Users.del-user-confirm">Supprimer l'utilisateur ?</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.demo">Demo</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.users">Utilisateurs</message>
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> 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.play-shuffled">Jouer aléatoirement</message>
<message id="Lms.Explore.random">Aléatoire</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-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.recently-played">Joués récemment</message>
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.star">Ajouter aux favoris</message> <message id="Lms.Explore.star">Ajouter aux favoris</message>
<message id="Lms.Explore.starred">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.tracks">Pistes</message>
<message id="Lms.Explore.type">Type</message> <message id="Lms.Explore.type">Type</message>
<message id="Lms.Explore.unstar">Retirer des favoris</message> <message id="Lms.Explore.unstar">Retirer des favoris</message>
@@ -155,6 +161,9 @@
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</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--> <!--Explore:Search-->
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message> <message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
@@ -163,7 +172,7 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Effacer</message> <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"> <message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">{1} piste ajoutée</plural> <plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural> <plural case="1">{1} pistes ajoutées</plural>
@@ -172,12 +181,12 @@
<message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message> <message id="Lms.PlayQueue.playqueue-full">Liste de lecture pleine!</message>
<message id="Lms.PlayQueue.radio-mode">Mode radio</message> <message id="Lms.PlayQueue.radio-mode">Mode radio</message>
<message id="Lms.PlayQueue.repeat">Repéter</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> <message id="Lms.PlayQueue.shuffle">Mélanger</message>
<!--PlayHistory--> <!--PlayHistory-->
<message id="Lms.PlayHistory.playhistory">Historique de lecture</message> <message id="Lms.PlayHistory.playhistory">Historique de lecture</message>
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message> <message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
+22 -7
View File
@@ -6,9 +6,11 @@
<message id="Lms.add">Aggiungi</message> <message id="Lms.add">Aggiungi</message>
<message id="Lms.administration">Amministrazione</message> <message id="Lms.administration">Amministrazione</message>
<message id="Lms.apply">Applica</message> <message id="Lms.apply">Applica</message>
<message id="Lms.discard">Annulla</message>
<message id="Lms.cancel">Annulla</message> <message id="Lms.cancel">Annulla</message>
<message id="Lms.create">Crea</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.loading">Caricamento...</message>
<message id="Lms.login">Login</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> <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.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.quit-other-session">Un'altra sessione è stata aperta. Vuoi riaprire questa?</message>
<message id="Lms.save">Salva</message> <message id="Lms.save">Salva</message>
<message id="Lms.user">Utente</message> <message id="Lms.user">Utente</message>
<!--Errors--> <!--Errors-->
@@ -31,6 +37,7 @@
<message id="Lms.Error.error-occurred">C'è stato un errore!</message> <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.go-home">Ritorna alla home</message>
<message id="Lms.Error.release-not-found">Album non trovato</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-allowed">Non sei autorizzato ad eseguire questa operazione</message>
<message id="Lms.Error.user-not-found">Utente non trovato</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.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.never">Mai</message>
<message id="Lms.Admin.Database.path">Cartella dei file multimediali</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">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.clusters">Basata sui tag</message>
<message id="Lms.Admin.Database.recommendation-engine-type.features">Basata sull'analisi acustica</message> <message id="Lms.Admin.Database.recommendation-engine-type.features">Basata sull'analisi acustica</message>
@@ -84,11 +92,8 @@
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">Nuovo utente</message> <message id="Lms.Admin.Users.add">Nuovo utente</message>
<message id="Lms.Admin.Users.admin">Amministratore</message> <message id="Lms.Admin.Users.admin">Amministratore</message>
<message id="Lms.Admin.Users.del">Elimina</message> <message id="Lms.Admin.Users.del-user-confirm">Elimina l'utente ?</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.demo">Demo</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.users">Utenti</message>
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> 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.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message>
<message id="Lms.Explore.random">Casuali</message> <message id="Lms.Explore.random">Casuali</message>
<message id="Lms.Explore.recently-added">Aggiunti di recente</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.recently-played">Riprodotti di recente</message>
<message id="Lms.Explore.releases">Album</message> <message id="Lms.Explore.releases">Album</message>
<message id="Lms.Explore.star">Aggiungi ai preferiti</message> <message id="Lms.Explore.star">Aggiungi ai preferiti</message>
<message id="Lms.Explore.starred">Preferiti</message> <message id="Lms.Explore.starred">Preferiti</message>
<message id="Lms.Explore.tracks">Tracce</message> <message id="Lms.Explore.tracks">Tracce</message>
<message id="Lms.Explore.type">Tipo</message> <message id="Lms.Explore.type">Tipo</message>
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message> <message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
@@ -154,6 +161,9 @@
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message> <message id="Lms.Explore.Release.disc">Disco {1}</message>
<!--Explore:TrackLists-->
<!--Explore:Search--> <!--Explore:Search-->
<message id="Lms.Explore.Search.search-placeholder">Ricerca...</message> <message id="Lms.Explore.Search.search-placeholder">Ricerca...</message>
@@ -162,7 +172,7 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Cancella</message> <message id="Lms.PlayQueue.clear">Cancella</message>
<message id="Lms.PlayQueue.nb-tracks">{1} tracce</message>
<message id="Lms.PlayQueue.nb-tracks-added"> <message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">Aggiunta {1} traccia</plural> <plural case="0">Aggiunta {1} traccia</plural>
<plural case="1">Aggiunte {1} tracce</plural> <plural case="1">Aggiunte {1} tracce</plural>
@@ -171,12 +181,12 @@
<message id="Lms.PlayQueue.playqueue-full">Coda di riproduzione piena!</message> <message id="Lms.PlayQueue.playqueue-full">Coda di riproduzione piena!</message>
<message id="Lms.PlayQueue.radio-mode">Modalità radio</message> <message id="Lms.PlayQueue.radio-mode">Modalità radio</message>
<message id="Lms.PlayQueue.repeat">Ripeti</message> <message id="Lms.PlayQueue.repeat">Ripeti</message>
<message id="Lms.PlayQueue.shuffle">Mischia</message> <message id="Lms.PlayQueue.shuffle">Mischia</message>
<!--PlayHistory--> <!--PlayHistory-->
<message id="Lms.PlayHistory.playhistory">Storico riproduzione</message> <message id="Lms.PlayHistory.playhistory">Storico riproduzione</message>
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message> <message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
@@ -192,6 +202,11 @@
<message id="Lms.Settings.replaygain-mode.release">Album</message> <message id="Lms.Settings.replaygain-mode.release">Album</message>
<message id="Lms.Settings.replaygain-preamp">Preamplificazione ReplayGain</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.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">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.all-artists">Tutti gli artisti</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
+21 -6
View File
@@ -6,9 +6,11 @@
<message id="Lms.add">添加</message> <message id="Lms.add">添加</message>
<message id="Lms.administration">管理</message> <message id="Lms.administration">管理</message>
<message id="Lms.apply">应用</message> <message id="Lms.apply">应用</message>
<message id="Lms.discard">撤销</message>
<message id="Lms.cancel">取消</message> <message id="Lms.cancel">取消</message>
<message id="Lms.create">新建</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.loading">加载中...</message>
<message id="Lms.login">登陆</message> <message id="Lms.login">登陆</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</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.passwords-dont-match">密码不匹配</message>
<message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message> <message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message>
<message id="Lms.save">保存</message> <message id="Lms.save">保存</message>
<message id="Lms.user">用户</message> <message id="Lms.user">用户</message>
<!--Errors--> <!--Errors-->
@@ -31,6 +37,7 @@
<message id="Lms.Error.error-occurred">发生错误!</message> <message id="Lms.Error.error-occurred">发生错误!</message>
<message id="Lms.Error.go-home">返回主页</message> <message id="Lms.Error.go-home">返回主页</message>
<message id="Lms.Error.release-not-found">发布未找到</message> <message id="Lms.Error.release-not-found">发布未找到</message>
<message id="Lms.Error.user-not-allowed">不允许执行此操作</message> <message id="Lms.Error.user-not-allowed">不允许执行此操作</message>
<message id="Lms.Error.user-not-found">未找到此用户</message> <message id="Lms.Error.user-not-found">未找到此用户</message>
@@ -85,11 +92,8 @@
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">新建用户</message> <message id="Lms.Admin.Users.add">新建用户</message>
<message id="Lms.Admin.Users.admin">管理员</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.demo">演示</message>
<message id="Lms.Admin.Users.edit">编辑</message>
<message id="Lms.Admin.Users.users">用户</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> <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.play-shuffled">随机播放</message>
<message id="Lms.Explore.random">随机</message> <message id="Lms.Explore.random">随机</message>
<message id="Lms.Explore.recently-added">最近添加</message> <message id="Lms.Explore.recently-added">最近添加</message>
<message id="Lms.Explore.recently-played">最近播放</message> <message id="Lms.Explore.recently-played">最近播放</message>
<message id="Lms.Explore.releases">专辑</message> <message id="Lms.Explore.releases">专辑</message>
<message id="Lms.Explore.star">收藏</message> <message id="Lms.Explore.star">收藏</message>
<message id="Lms.Explore.starred">已收藏</message> <message id="Lms.Explore.starred">已收藏</message>
<message id="Lms.Explore.tracks">音轨</message> <message id="Lms.Explore.tracks">音轨</message>
<message id="Lms.Explore.type">类型</message> <message id="Lms.Explore.type">类型</message>
<message id="Lms.Explore.unstar">取消收藏</message> <message id="Lms.Explore.unstar">取消收藏</message>
@@ -155,6 +161,9 @@
<message id="Lms.Explore.Release.copyright">版权所有</message> <message id="Lms.Explore.Release.copyright">版权所有</message>
<message id="Lms.Explore.Release.disc">唱片 {1}</message> <message id="Lms.Explore.Release.disc">唱片 {1}</message>
<!--Explore:TrackLists-->
<!--Explore:Search--> <!--Explore:Search-->
<message id="Lms.Explore.Search.search-placeholder">搜索中...</message> <message id="Lms.Explore.Search.search-placeholder">搜索中...</message>
@@ -163,7 +172,7 @@
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">清除</message> <message id="Lms.PlayQueue.clear">清除</message>
<message id="Lms.PlayQueue.nb-tracks">{1} 音轨</message>
<message id="Lms.PlayQueue.nb-tracks-added"> <message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">添加 {1} 音轨</plural> <plural case="0">添加 {1} 音轨</plural>
<plural case="1">已添加 {1} 音轨</plural> <plural case="1">已添加 {1} 音轨</plural>
@@ -172,6 +181,7 @@
<message id="Lms.PlayQueue.playqueue-full">播放队列已满!</message> <message id="Lms.PlayQueue.playqueue-full">播放队列已满!</message>
<message id="Lms.PlayQueue.radio-mode">广播模式</message> <message id="Lms.PlayQueue.radio-mode">广播模式</message>
<message id="Lms.PlayQueue.repeat">循环播放</message> <message id="Lms.PlayQueue.repeat">循环播放</message>
<message id="Lms.PlayQueue.shuffle">随机播放</message> <message id="Lms.PlayQueue.shuffle">随机播放</message>
<!--PlayHistory--> <!--PlayHistory-->
@@ -192,6 +202,11 @@
<message id="Lms.Settings.replaygain-mode.release">专辑</message> <message id="Lms.Settings.replaygain-mode.release">专辑</message>
<message id="Lms.Settings.replaygain-preamp">ReplayGain 放大器</message> <message id="Lms.Settings.replaygain-preamp">ReplayGain 放大器</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">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">歌手列表模式</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
+6 -1
View File
@@ -9,9 +9,14 @@
</div> </div>
</message> </message>
<message id="Lms.infinite-scrolling-container"> <message id="Lms.infinite-scrolling-container.template">
${elements} ${elements}
${loading-indicator} ${loading-indicator}
</message> </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> </messages>
+59 -14
View File
@@ -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.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.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.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"> <message id="Lms.PlayQueue.template">
<div class="small text-muted mb-2">${track-count} · ${duration}</div>
<div class="mb-3"> <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"> <div class="btn-group me-2">
${repeat-btn class="btn-check"} ${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> <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"} ${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> <label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.radio-mode}" for="${id:radio-btn}">${tr:Lms.PlayQueue.template.radio-btn}</label>
</div> </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> </div>
${entries} ${entries}
</message> </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"> <message id="Lms.PlayQueue.template.entry">
<div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2"> <div class="d-flex align-items-center rounded Lms-bg-dark-hover mb-2">
<div class="p-1 Lms-cover-small"> <div class="p-1 Lms-cover-small">
@@ -44,14 +44,14 @@
</div> </div>
<div class="p-2 d-flex"> <div class="p-2 d-flex">
${play-btn class="btn btn-sm btn-outline-secondary border-0"} ${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"> <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"} ${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}"> <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${star class="btn dropdown-item"}</li> <li>${star class="dropdown-item"}</li>
<li>${download class="btn dropdown-item"}</li> <li>${download class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
${del-btn class="btn btn-sm btn-outline-secondary border-0"}
</div> </div>
</div> </div>
</message> </message>
@@ -60,4 +60,49 @@
${artist} ${artist}
</message> </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> </messages>
+7 -7
View File
@@ -21,13 +21,13 @@
${play-btn class="btn btn-primary"} ${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"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-shuffled class="btn dropdown-item"}</li> <li>${play-shuffled class="dropdown-item"}</li>
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${star class="btn dropdown-item"}</li> <li>${star class="dropdown-item"}</li>
${<if-has-mbid>} ${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li> <li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
${</if-has-mbid>} ${</if-has-mbid>}
<li>${download class="btn dropdown-item"}</li> <li>${download class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -74,9 +74,9 @@
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"} ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}"> <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${star class="btn dropdown-item"}</li> <li>${star class="dropdown-item"}</li>
<li>${download class="btn dropdown-item"}</li> <li>${download class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
+2 -2
View File
@@ -28,8 +28,8 @@
${play-btn class="btn btn-primary"} ${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"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${play-shuffled class="btn dropdown-item"}</li> <li>${play-shuffled class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
+40
View File
@@ -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>
+28
View File
@@ -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
View File
@@ -29,8 +29,8 @@
${play-btn class="btn btn-primary"} ${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"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${play-shuffled class="btn dropdown-item"}</li> <li>${play-shuffled class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -54,9 +54,9 @@
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"} ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}"> <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<li>${play-last class="btn dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${star class="btn dropdown-item"}</li> <li>${star class="dropdown-item"}</li>
<li>${download class="btn dropdown-item"}</li> <li>${download class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -45,7 +45,7 @@ namespace Auth
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN); 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); user.modify()->setType(type);
} }
@@ -60,7 +60,7 @@ namespace Auth
if (!user) if (!user)
throw Exception {"User deleted"}; 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(); LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
+12 -16
View File
@@ -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 std::size_t
Artist::getCount(Session& session) 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; 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 static
Wt::Dbo::Query<ArtistId> Wt::Dbo::Query<ArtistId>
createQuery(Session& session, const Artist::FindParameters& params) 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) for (std::string_view keyword : params.keywords)
{ {
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + escapeLikeKeyword(keyword) + "%"); query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
} }
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
{ {
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + escapeLikeKeyword(keyword) + "%"); query.bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
} }
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")"); query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
@@ -193,7 +188,7 @@ Artist::findAllOrphans(Session& session, Range range)
session.checkSharedLocked(); 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)")}; 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> RangeResults<ArtistId>
@@ -202,7 +197,7 @@ Artist::find(Session& session, const FindParameters& params)
session.checkSharedLocked(); session.checkSharedLocked();
auto query {createQuery(session, params)}; auto query {createQuery(session, params)};
return execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
RangeResults<ArtistId> RangeResults<ArtistId>
@@ -246,10 +241,11 @@ Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range r
.bind(getId()) .bind(getId())
.groupBy("a.id") .groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()")}; .orderBy("COUNT(*) DESC, RANDOM()")};
for (TrackArtistLinkType type : artistLinkTypes) for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type); query.bind(type);
return execQuery(query, range); return Utils::execQuery(query, range);
} }
std::vector<std::vector<Cluster::pointer>> std::vector<std::vector<Cluster::pointer>>
@@ -38,12 +38,7 @@ namespace Database
AuthToken::pointer AuthToken::pointer
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user) AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
{ {
session.checkUniqueLocked(); return session.getDboSession().add(std::unique_ptr<AuthToken> {new AuthToken {value, expiry, user}});
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
session.getDboSession().flush();
return res;
} }
void void
+18 -28
View File
@@ -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 std::size_t
Cluster::getCount(Session& session) Cluster::getCount(Session& session)
{ {
@@ -44,24 +50,13 @@ Cluster::getCount(Session& session)
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster"); 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> RangeResults<ClusterId>
Cluster::find(Session& session, Range range) Cluster::find(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")}; auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ClusterId> RangeResults<ClusterId>
@@ -70,7 +65,7 @@ Cluster::findOrphans(Session& session, Range range)
session.checkSharedLocked(); 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)")}; 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 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") auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(getId())}; .where("c.id = ?").bind(getId())};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
std::size_t 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 std::size_t
ClusterType::getCount(Session& session) 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") " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
.where("c.id IS NULL")}; .where("c.id IS NULL")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ClusterTypeId> RangeResults<ClusterTypeId>
@@ -144,7 +145,7 @@ ClusterType::findUsed(Session& session, Range range)
"SELECT DISTINCT c_t.id from cluster_type c_t") "SELECT DISTINCT c_t.id from cluster_type c_t")
.join("cluster c ON c_t.id = c.cluster_type_id")}; .join("cluster c ON c_t.id = c.cluster_type_id")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
ClusterType::pointer ClusterType::pointer
@@ -170,18 +171,7 @@ ClusterType::find(Session& session, Range range)
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")}; auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")};
return execQuery(query, range); return Utils::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;
} }
Cluster::pointer Cluster::pointer
+13 -18
View File
@@ -141,6 +141,12 @@ namespace Database
, _track {getDboPtr(track)} , _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 std::size_t
Listen::getCount(Session& session) Listen::getCount(Session& session)
{ {
@@ -172,7 +178,7 @@ namespace Database
if (parameters.scrobblingState) if (parameters.scrobblingState)
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
return execQuery(query, parameters.range); return Utils::execQuery(query, parameters.range);
} }
Listen::pointer Listen::pointer
@@ -188,17 +194,6 @@ namespace Database
.resultValue(); .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> RangeResults<ArtistId>
Listen::getTopArtists(Session& session, Listen::getTopArtists(Session& session,
UserId userId, UserId userId,
@@ -213,7 +208,7 @@ namespace Database
.orderBy("COUNT(a.id) DESC") .orderBy("COUNT(a.id) DESC")
.groupBy("a.id")}; .groupBy("a.id")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId>
@@ -227,7 +222,7 @@ namespace Database
.orderBy("COUNT(r.id) DESC") .orderBy("COUNT(r.id) DESC")
.groupBy("r.id")}; .groupBy("r.id")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> RangeResults<TrackId>
@@ -241,7 +236,7 @@ namespace Database
.orderBy("COUNT(t.id) DESC") .orderBy("COUNT(t.id) DESC")
.groupBy("t.id")}; .groupBy("t.id")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ArtistId> RangeResults<ArtistId>
@@ -256,7 +251,7 @@ namespace Database
.groupBy("a.id").having("l.date_time = MAX(l.date_time)") .groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")}; .orderBy("l.date_time DESC")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId>
@@ -270,7 +265,7 @@ namespace Database
.groupBy("r.id").having("l.date_time = MAX(l.date_time)") .groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")}; .orderBy("l.date_time DESC")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> RangeResults<TrackId>
@@ -284,7 +279,7 @@ namespace Database
.groupBy("t.id").having("l.date_time = MAX(l.date_time)") .groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")}; .orderBy("l.date_time DESC")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
+11 -1
View File
@@ -576,12 +576,21 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
migrateFromV34(Session& session) migrateFromV34(Session& session)
{ {
// Add scrobbling state // Add scrobbling state
// By default, everythin needs to be sent // By default, everything needs to be sent
session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")"); 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_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)) + ")"); 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 void
doDbMigration(Session& session) doDbMigration(Session& session)
{ {
@@ -623,6 +632,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
{32, migrateFromV32}, {32, migrateFromV32},
{33, migrateFromV33}, {33, migrateFromV33},
{34, migrateFromV34}, {34, migrateFromV34},
{35, migrateFromV35},
}; };
while (1) while (1)
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {35}; static constexpr Version LMS_DATABASE_VERSION {36};
class VersionInfo class VersionInfo
{ {
public: public:
+11 -16
View File
@@ -57,7 +57,7 @@ createQuery(Session& session, const Release::FindParameters& params)
} }
for (std::string_view keyword : params.keywords) 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()) if (params.starringUser.isValid())
{ {
@@ -172,6 +172,12 @@ _MBID {MBID ? MBID->getAsString() : ""}
{ {
} }
Release::pointer
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
{
return session.getDboSession().add(std::unique_ptr<Release> {new Release {name, MBID}});
}
std::vector<Release::pointer> std::vector<Release::pointer>
Release::find(Session& session, const std::string& name) Release::find(Session& session, const std::string& name)
{ {
@@ -214,17 +220,6 @@ Release::exists(Session& session, ReleaseId id)
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; 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 std::size_t
Release::getCount(Session& session) Release::getCount(Session& session)
{ {
@@ -238,7 +233,7 @@ Release::findOrderedByArtist(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
// TODO merge with execQuery // TODO merge with find
auto query {session.getDboSession().query<ReleaseId>( auto query {session.getDboSession().query<ReleaseId>(
"SELECT DISTINCT r.id FROM release r" "SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON r.id = t.release_id" " INNER JOIN track t ON r.id = t.release_id"
@@ -246,7 +241,7 @@ Release::findOrderedByArtist(Session& session, Range range)
" INNER JOIN artist a ON t_a_l.artist_id = a.id") " INNER JOIN artist a ON t_a_l.artist_id = a.id")
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")}; .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId>
@@ -255,7 +250,7 @@ Release::findOrphans(Session& session, Range range)
session.checkSharedLocked(); 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")}; 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> RangeResults<ReleaseId>
@@ -265,7 +260,7 @@ Release::find(Session& session, const FindParameters& params)
auto query {createQuery(session, params)}; auto query {createQuery(session, params)};
return execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
std::optional<std::size_t> std::optional<std::size_t>
@@ -120,7 +120,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clu
if (!clusterType) if (!clusterType)
{ {
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName; LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
clusterType = ClusterType::create(session, clusterTypeName); clusterType = session.create<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType)); _clusterTypes.insert(getDboPtr(clusterType));
needRescan = true; needRescan = true;
@@ -36,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 std::size_t
StarredArtist::getCount(Session& session) StarredArtist::getCount(Session& session)
{ {
@@ -61,20 +67,9 @@ namespace Database
.resultValue(); .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 void
StarredArtist::setDateTime(const Wt::WDateTime& dateTime) StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
{ {
_dateTime = normalizeDateTime(dateTime); _dateTime = Utils::normalizeDateTime(dateTime);
} }
} }
@@ -36,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 std::size_t
StarredRelease::getCount(Session& session) StarredRelease::getCount(Session& session)
{ {
@@ -61,20 +67,9 @@ namespace Database
.resultValue(); .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 void
StarredRelease::setDateTime(const Wt::WDateTime& dateTime) StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
{ {
_dateTime = normalizeDateTime(dateTime); _dateTime = Utils::normalizeDateTime(dateTime);
} }
} }
@@ -36,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 std::size_t
StarredTrack::getCount(Session& session) StarredTrack::getCount(Session& session)
{ {
@@ -75,23 +81,12 @@ namespace Database
if (params.user.isValid()) if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user); query.where("s_t.user_id = ?").bind(params.user);
return execQuery(query, params.range); return Utils::execQuery(query, params.range);
}
StarredTrack::pointer
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
{
session.checkUniqueLocked();
StarredTrack::pointer res {session.getDboSession().add(std::make_unique<StarredTrack>(track, user, scrobbler))};
session.getDboSession().flush();
return res;
} }
void void
StarredTrack::setDateTime(const Wt::WDateTime& dateTime) StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
{ {
_dateTime = normalizeDateTime(dateTime); _dateTime = Utils::normalizeDateTime(dateTime);
} }
} }
+19 -14
View File
@@ -43,10 +43,10 @@ createQuery(Session& session, const Track::FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query {session.getDboSession().query<TrackId>("SELECT DISTINCT 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) 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()) if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter); query.where("t.file_last_write > ?").bind(params.writtenAfter);
@@ -110,6 +110,13 @@ createQuery(Session& session, const Track::FindParameters& params)
else if (params.release.isValid()) else if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); 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) switch (params.sortMethod)
{ {
case TrackSortMethod::None: case TrackSortMethod::None:
@@ -133,6 +140,9 @@ createQuery(Session& session, const Track::FindParameters& params)
case TrackSortMethod::Release: case TrackSortMethod::Release:
query.orderBy("t.disc_number,t.track_number"); query.orderBy("t.disc_number,t.track_number");
break; break;
case TrackSortMethod::TrackList:
assert(params.trackList.isValid());
query.orderBy("t_l.id");
} }
return query; return query;
@@ -146,12 +156,7 @@ Track::Track(const std::filesystem::path& p)
Track::pointer Track::pointer
Track::create(Session& session, const std::filesystem::path& p) Track::create(Session& session, const std::filesystem::path& p)
{ {
session.checkUniqueLocked(); return session.getDboSession().add(std::unique_ptr<Track> {new Track {p}});
Track::pointer res {session.getDboSession().add(std::make_unique<Track>(p))};
session.getDboSession().flush();
return res;
} }
std::size_t std::size_t
@@ -209,7 +214,7 @@ Track::findPaths(Session& session, Range range)
// TODO Dbo traits on filesystem // TODO Dbo traits on filesystem
auto query {session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track")}; 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; RangeResults<PathResult> res;
res.range = queryResults.range; res.range = queryResults.range;
@@ -233,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)") 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")}; .orderBy("track.release_id,track.disc_number,track.track_number,track.recording_mbid")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> RangeResults<TrackId>
@@ -245,7 +250,7 @@ Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
.where("LENGTH(t.recording_mbid) > 0") .where("LENGTH(t.recording_mbid) > 0")
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")}; .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
std::vector<Cluster::pointer> std::vector<Cluster::pointer>
@@ -274,7 +279,7 @@ Track::find(Session& session, const FindParameters& parameters)
auto query {createQuery(session, parameters)}; auto query {createQuery(session, parameters)};
return execQuery(query, parameters.range); return Utils::execQuery(query, parameters.range);
} }
RangeResults<TrackId> RangeResults<TrackId>
@@ -287,7 +292,7 @@ Track::findByNameAndReleaseName(Session& session, std::string_view trackName, st
.where("t.name = ?").bind(trackName) .where("t.name = ?").bind(trackName)
.where("r.name = ?").bind(releaseName)}; .where("r.name = ?").bind(releaseName)};
return execQuery(query, Range {}); return Utils::execQuery(query, Range {});
} }
RangeResults<TrackId> RangeResults<TrackId>
@@ -318,7 +323,7 @@ Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, R
for (TrackId trackId : tracks) for (TrackId trackId : tracks)
query.bind(trackId); query.bind(trackId);
return execQuery(query, range); return Utils::execQuery(query, range);
} }
void 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 std::size_t
TrackBookmark::getCount(Session& session) TrackBookmark::getCount(Session& session)
{ {
@@ -41,18 +47,6 @@ TrackBookmark::getCount(Session& session)
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark"); 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> RangeResults<TrackBookmarkId>
TrackBookmark::find(Session& session, UserId userId, Range range) 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") auto query {session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
.where("user_id = ?").bind(userId)}; .where("user_id = ?").bind(userId)};
return execQuery(query, range); return Utils::execQuery(query, range);
} }
TrackBookmark::pointer 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 std::size_t
TrackFeatures::getCount(Session& session) 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")}; auto query {session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features")};
return execQuery(query, range); return Utils::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;
} }
FeatureValues FeatureValues
+118 -37
View File
@@ -35,24 +35,21 @@
namespace Database { namespace Database {
TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user) TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
: _name {name}, : _name {name}
_type {type}, , _type {type}
_isPublic {isPublic}, , _isPublic {isPublic}
_user {getDboPtr(user)} , _creationDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
, _lastModifiedDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
, _user {getDboPtr(user)}
{ {
assert(user);
} }
TrackList::pointer 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(); return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList {name, type, isPublic, user}});
assert(user);
TrackList::pointer res {session.getDboSession().add( std::make_unique<TrackList>(name, type, isPublic, user) )};
session.getDboSession().flush();
return res;
} }
std::size_t std::size_t
@@ -65,7 +62,7 @@ TrackList::getCount(Session& session)
TrackList::pointer 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(); session.checkSharedLocked();
assert(userId.isValid()); assert(userId.isValid());
@@ -77,28 +74,54 @@ TrackList::find(Session& session, std::string_view name, Type type, UserId userI
} }
RangeResults<TrackListId> RangeResults<TrackListId>
TrackList::find(Session& session, UserId userId, Range range) TrackList::find(Session& session, const FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist") auto query {session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l")};
.where("user_id = ?").bind(userId)
.orderBy("name COLLATE NOCASE")};
return execQuery(query, range); if (params.user.isValid())
} query.where("t_l.user_id = ?").bind(params.user);
RangeResults<TrackListId> if (params.type)
TrackList::find(Session& session, UserId userId, Type type, Range range) query.where("t_l.type = ?").bind(*params.type);
{
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist") if (!params.clusters.empty())
.where("user_id = ?").bind(userId) {
.where("type = ?").bind(type) query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
.orderBy("name COLLATE NOCASE")}; 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 TrackList::pointer
@@ -157,7 +180,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTim
return session()->find<TrackListEntry>() return session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(getId()) .where("tracklist_id = ?").bind(getId())
.where("track_id = ?").bind(track->getId()) .where("track_id = ?").bind(track->getId())
.where("date_time = ?").bind(normalizeDateTime(dateTime)) .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime))
.resultValue(); .resultValue();
} }
@@ -443,6 +466,57 @@ TrackList::getClusters() const
return std::vector<Cluster::pointer>(res.begin(), res.end()); 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 bool
TrackList::hasTrack(TrackId trackId) const TrackList::hasTrack(TrackId trackId) const
{ {
@@ -580,23 +654,30 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
} }
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime) TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
: _dateTime {normalizeDateTime(dateTime)} : _dateTime {Utils::normalizeDateTime(dateTime)}
, _track {getDboPtr(track)} , _track {getDboPtr(track)}
, _tracklist {getDboPtr(tracklist)} , _tracklist {getDboPtr(tracklist)}
{ {
assert(track);
assert(tracklist);
} }
TrackListEntry::pointer TrackListEntry::pointer
TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime) TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
{ {
session.checkUniqueLocked(); return session.getDboSession().add(std::unique_ptr<TrackListEntry>( new TrackListEntry {track, tracklist, dateTime}));
assert(track); }
assert(tracklist);
auto res = session.getDboSession().add(std::make_unique<TrackListEntry>( track, tracklist, dateTime)); void
session.getDboSession().flush(); 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 TrackListEntry::pointer
+7 -12
View File
@@ -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 std::size_t
User::getCount(Session& session) User::getCount(Session& session)
{ {
@@ -53,7 +59,7 @@ User::find(Session& session, const FindParameters& params)
if (params.scrobbler) if (params.scrobbler)
query.where("scrobbler = ?").bind(*params.scrobbler); query.where("scrobbler = ?").bind(*params.scrobbler);
return execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
User::pointer User::pointer
@@ -64,17 +70,6 @@ User::findDemoUser(Session& session)
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue(); 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::pointer
User::find(Session& session, UserId id) User::find(Session& session, UserId id)
{ {
+2 -3
View File
@@ -21,7 +21,7 @@
#include "utils/String.hpp" #include "utils/String.hpp"
namespace Database namespace Database::Utils
{ {
std::string std::string
escapeLikeKeyword(std::string_view keyword) escapeLikeKeyword(std::string_view keyword)
@@ -35,6 +35,5 @@ namespace Database
// force second resolution // force second resolution
return Wt::WDateTime::fromTime_t(dateTime.toTime_t()); return Wt::WDateTime::fromTime_t(dateTime.toTime_t());
} }
} // namespace Database::Utils
} // namespace Database
+2 -2
View File
@@ -27,7 +27,7 @@
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
namespace Database namespace Database::Utils
{ {
#define ESCAPE_CHAR_STR "\\" #define ESCAPE_CHAR_STR "\\"
static inline constexpr char escapeChar {'\\'}; static inline constexpr char escapeChar {'\\'};
@@ -82,5 +82,5 @@ namespace Database
} }
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); 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() = default;
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
// Accessors // Accessors
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
@@ -101,9 +100,6 @@ class Artist : public Object<Artist, ArtistId>
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
void setSortName(const std::string& sortName); void setSortName(const std::string& sortName);
// Create
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
@@ -118,6 +114,11 @@ class Artist : public Object<Artist, ArtistId>
private: private:
static const std::size_t _maxNameLength = 128; 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 _name;
std::string _sortName; std::string _sortName;
std::string _MBID; // Musicbrainz Identifier std::string _MBID; // Musicbrainz Identifier
@@ -36,10 +36,8 @@ namespace Database
{ {
public: public:
AuthToken() = default; AuthToken() = default;
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
// Utility // 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 void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
static pointer find(Session& session, std::string_view value); static pointer find(Session& session, std::string_view value);
@@ -57,6 +55,10 @@ namespace Database
} }
private: 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; std::string _value;
Wt::WDateTime _expiry; Wt::WDateTime _expiry;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
@@ -42,7 +42,6 @@ class Cluster : public Object<Cluster, ClusterId>
{ {
public: public:
Cluster() = default; Cluster() = default;
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
// Find utility // Find utility
static std::size_t getCount(Session& session); 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 pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphans(Session& session, Range range); static RangeResults<ClusterId> findOrphans(Session& session, Range range);
// Create utility
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; } ObjectPtr<ClusterType> getType() const { return _clusterType; }
@@ -72,6 +68,10 @@ class Cluster : public Object<Cluster, ClusterId>
} }
private: 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; static const std::size_t _maxNameLength = 128;
std::string _name; std::string _name;
@@ -85,7 +85,6 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
{ {
public: public:
ClusterType() = default; ClusterType() = default;
ClusterType(std::string_view name);
// Getters // Getters
static std::size_t getCount(Session& session); 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> findOrphans(Session& session, Range range);
static RangeResults<ClusterTypeId> findUsed(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); static void remove(Session& session, const std::string& name);
// Accessors // Accessors
@@ -112,6 +110,10 @@ class ClusterType : public Object<ClusterType, ClusterTypeId>
} }
private: 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; static const std::size_t _maxNameLength = 128;
std::string _name; std::string _name;
@@ -42,7 +42,6 @@ class Listen : public Object<Listen, ListenId>
{ {
public: public:
Listen() = default; Listen() = default;
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
struct FindParameters 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 pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters); 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 // Stats
static RangeResults<ArtistId> getTopArtists(Session& session, static RangeResults<ArtistId> getTopArtists(Session& session,
UserId userId, UserId userId,
@@ -120,6 +116,10 @@ class Listen : public Object<Listen, ListenId>
} }
private: 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; Wt::WDateTime _dateTime;
Scrobbler _scrobbler; Scrobbler _scrobbler;
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <Wt/WSignal.h>
#include <Wt/Dbo/ptr.h> #include <Wt/Dbo/ptr.h>
#include "services/database/IdType.hpp" #include "services/database/IdType.hpp"
@@ -38,7 +39,11 @@ namespace Database
bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; } bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; }
auto modify() { return _obj.modify(); } auto modify() { return _obj.modify(); }
void remove() { _obj.remove(); } void remove()
{
_obj.modify()->onPreRemove();
_obj.remove();
}
private: private:
template <typename, typename> friend class Object; template <typename, typename> friend class Object;
@@ -61,6 +66,10 @@ namespace Database
typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete; typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete;
protected: protected:
template <typename> friend class ObjectPtr;
virtual void onPostCreated() {}
virtual void onPreRemove() {}
// Can get raw dbo ptr only from Objects // Can get raw dbo ptr only from Objects
template <typename SomeObject> template <typename SomeObject>
static static
@@ -79,7 +79,6 @@ class Release : public Object<Release, ReleaseId>
}; };
Release() = default; Release() = default;
Release(const std::string& name, const std::optional<UUID>& MBID = {});
// Accessors // Accessors
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
@@ -98,9 +97,6 @@ class Release : public Object<Release, ReleaseId>
// size is the max number of cluster per cluster type // 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; 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 // Utility functions
std::optional<int> getReleaseYear(bool originalDate = false) const; std::optional<int> getReleaseYear(bool originalDate = false) const;
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
@@ -133,6 +129,10 @@ class Release : public Object<Release, ReleaseId>
} }
private: 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}; static const std::size_t _maxNameLength {128};
std::string _name; std::string _name;
@@ -39,7 +39,8 @@ class ScanSettings : public Object<ScanSettings, ScanSettingsId>
{ {
public: public:
// Do not modify values (just add) // Do not modify values (just add)
enum class UpdatePeriod { enum class UpdatePeriod
{
Never = 0, Never = 0,
Daily, Daily,
Weekly, Weekly,
@@ -19,16 +19,15 @@
#pragma once #pragma once
#include <mutex>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/SqlConnectionPool.h> #include <Wt/Dbo/SqlConnectionPool.h>
#include "utils/RecursiveSharedMutex.hpp" #include "utils/RecursiveSharedMutex.hpp"
#include "services/database/Object.hpp"
namespace Database namespace Database
{ {
class UniqueTransaction class UniqueTransaction
{ {
private: private:
@@ -73,11 +72,22 @@ namespace Database
Wt::Dbo::Session& getDboSession() { return _session; } Wt::Dbo::Session& getDboSession() { return _session; }
Db& getDb() { return _db; } 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();
res.modify()->onPostCreated();
return res;
}
private: private:
Db& _db; Db& _db;
Wt::Dbo::Session _session; Wt::Dbo::Session _session;
}; };
} // namespace Database } // namespace Database
@@ -38,16 +38,12 @@ namespace Database
{ {
public: public:
StarredArtist() = default; StarredArtist() = default;
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
// Search utility // Search utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredArtistId id); static pointer find(Session& session, StarredArtistId id);
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler); 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 // Accessors
ObjectPtr<Artist> getArtist() const { return _artist; } ObjectPtr<Artist> getArtist() const { return _artist; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
@@ -71,6 +67,10 @@ namespace Database
} }
private: 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 Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
@@ -38,16 +38,12 @@ namespace Database
{ {
public: public:
StarredRelease() = default; StarredRelease() = default;
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
// Search utility // Search utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, StarredReleaseId id); static pointer find(Session& session, StarredReleaseId id);
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler); 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 // Accessors
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
@@ -71,6 +67,10 @@ namespace Database
} }
private: 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 Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
@@ -39,7 +39,6 @@ namespace Database
{ {
public: public:
StarredTrack() = default; StarredTrack() = default;
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
struct FindParameters struct FindParameters
{ {
@@ -59,9 +58,6 @@ namespace Database
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler); static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams); static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
// Create utility
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
// Accessors // Accessors
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
@@ -85,6 +81,10 @@ namespace Database
} }
private: 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 Scrobbler _scrobbler; // for which scrobbler
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
Wt::WDateTime _dateTime; // when it was starred Wt::WDateTime _dateTime; // when it was starred
@@ -39,6 +39,7 @@
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp" #include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp" #include "services/database/TrackId.hpp"
#include "services/database/TrackListId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "services/database/UserId.hpp" #include "services/database/UserId.hpp"
@@ -69,6 +70,8 @@ class Track : public Object<Track, TrackId>
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease {}; // only tracks that do not belong to a release bool nonRelease {}; // only tracks that do not belong to a release
ReleaseId release; // matching this 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& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
@@ -79,6 +82,8 @@ class Track : public Object<Track, TrackId>
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; 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 struct PathResult
@@ -88,7 +93,6 @@ class Track : public Object<Track, TrackId>
}; };
Track() = default; Track() = default;
Track(const std::filesystem::path& p);
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
@@ -104,9 +108,6 @@ class Track : public Object<Track, TrackId>
static RangeResults<TrackId> findRecordingMBIDDuplicates(Session& session, Range range); static RangeResults<TrackId> findRecordingMBIDDuplicates(Session& session, Range range);
static RangeResults<TrackId> findWithRecordingMBIDAndMissingFeatures(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 // Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; } void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(int num) { _trackNumber = num; } void setTrackNumber(int num) { _trackNumber = num; }
@@ -193,6 +194,10 @@ class Track : public Object<Track, TrackId>
} }
private: 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 _maxNameLength = 128;
static const std::size_t _maxCopyrightLength = 128; static const std::size_t _maxCopyrightLength = 128;
static const std::size_t _maxCopyrightURLLength = 128; static const std::size_t _maxCopyrightURLLength = 128;
@@ -41,10 +41,6 @@ class TrackBookmark : public Object<TrackBookmark, TrackBookmarkId>
{ {
public: public:
TrackBookmark () = default; TrackBookmark () = default;
TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track);
// utility
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track);
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session); 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, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
} }
private: 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; static const std::size_t _maxCommentLength = 128;
std::chrono::duration<int, std::milli> _offset; std::chrono::duration<int, std::milli> _offset;
@@ -46,7 +46,6 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
{ {
public: public:
TrackFeatures() = default; TrackFeatures() = default;
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
// Find utilities // Find utilities
static std::size_t getCount(Session& session); 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 pointer find(Session& session, TrackId trackId);
static RangeResults<TrackFeaturesId> find(Session& session, Range range); 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; FeatureValues getFeatureValues(const FeatureName& feature) const;
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
@@ -71,6 +67,10 @@ class TrackFeatures : public Object<TrackFeatures, TrackFeaturesId>
} }
private: 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; std::string _data;
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
}; };
@@ -38,6 +38,7 @@ namespace Database {
class Artist; class Artist;
class Cluster; class Cluster;
class ClusterType;
class Release; class Release;
class Session; class Session;
class Track; class Track;
@@ -47,14 +48,7 @@ class User;
class TrackList : public Object<TrackList, TrackListId> class TrackList : public Object<TrackList, TrackListId>
{ {
public: public:
enum class Type
{
Playlist, // user controlled playlists
Internal, // internal usage (current playqueue, history, ...)
};
TrackList() = default; TrackList() = default;
TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<User> user);
// Stats utility // Stats utility
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const; std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
@@ -62,25 +56,36 @@ class TrackList : public Object<TrackList, TrackListId>
std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
// Search utility // Search utility
static std::size_t getCount(Session& session); struct FindParameters
static pointer find(Session& session, std::string_view name, Type type, UserId userId); {
static pointer find(Session& session, TrackListId tracklistId); std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
static RangeResults<TrackListId> find(Session& session, UserId userId, Range range); Range range;
static RangeResults<TrackListId> find(Session& session, UserId userId, Type type, Range range); std::optional<TrackListType> type;
UserId user; // only tracklists owned by this user
TrackListSortMethod sortMethod {TrackListSortMethod::None};
// Create utility FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr<User> user); 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 // Accessors
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
bool isPublic() const { return _isPublic; } bool isPublic() const { return _isPublic; }
Type getType() const { return _type; } TrackListType getType() const { return _type; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
// Modifiers // Modifiers
void setName(const std::string& name) { _name = name; } void setName(const std::string& name) { _name = name; }
void setIsPublic(bool isPublic) { _isPublic = isPublic; } void setIsPublic(bool isPublic) { _isPublic = isPublic; }
void clear() { _entries.clear(); } void clear() { _entries.clear(); }
void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { _lastModifiedDateTime = dateTime; }
// Get tracks, ordered by position // Get tracks, ordered by position
bool isEmpty() const; bool isEmpty() const;
@@ -103,6 +108,7 @@ class TrackList : public Object<TrackList, TrackListId>
// Get clusters, order by occurence // Get clusters, order by occurence
std::vector<ObjectPtr<Cluster>> getClusters() const; 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; bool hasTrack(TrackId trackId) const;
@@ -112,18 +118,26 @@ class TrackList : public Object<TrackList, TrackListId>
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _type, "type"); Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _isPublic, "public"); 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::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
} }
private: 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; std::string _name;
Type _type {Type::Playlist}; TrackListType _type {TrackListType::Playlist};
bool _isPublic {false}; bool _isPublic {false};
Wt::WDateTime _creationDateTime;
Wt::WDateTime _lastModifiedDateTime;
Wt::Dbo::ptr<User> _user; Wt::Dbo::ptr<User> _user;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries; Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
@@ -133,15 +147,13 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
{ {
public: public:
TrackListEntry() = default; TrackListEntry() = default;
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist); void onPostCreated() override;
void onPreRemove() override;
// find utility // find utility
static pointer getById(Session& session, TrackListEntryId id); 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 // Accessors
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; } const Wt::WDateTime& getDateTime() const { return _dateTime; }
@@ -156,6 +168,11 @@ class TrackListEntry : public Object<TrackListEntry, TrackListEntryId>
} }
private: 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::WDateTime _dateTime; // optional date time
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackList> _tracklist; Wt::Dbo::ptr<TrackList> _tracklist;
@@ -104,6 +104,13 @@ namespace Database
StarredDateDesc, StarredDateDesc,
}; };
enum class TrackListSortMethod
{
None,
Name,
LastModifiedDesc,
};
enum class TrackSortMethod enum class TrackSortMethod
{ {
None, None,
@@ -113,6 +120,7 @@ namespace Database
Name, Name,
DateDescAndRelease, DateDescAndRelease,
Release, // order by disc/track number Release, // order by disc/track number
TrackList, // order by asc order in tracklist
}; };
enum class TrackArtistLinkType enum class TrackArtistLinkType
@@ -177,5 +185,11 @@ namespace Database
ReleaseArtists = 1, ReleaseArtists = 1,
TrackArtists = 2, 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}; static inline const Scrobbler defaultScrobbler {Scrobbler::Internal};
User() = default; 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 std::size_t getCount(Session& session);
static pointer find(Session& session, UserId id); static pointer find(Session& session, UserId id);
@@ -137,6 +133,10 @@ class User : public Object<User, UserId>
} }
private: private:
friend class Session;
User(std::string_view loginName);
static pointer create(Session& session, std::string_view loginName);
std::string _loginName; std::string _loginName;
std::string _passwordSalt; std::string _passwordSalt;
std::string _passwordHash; std::string _passwordHash;
@@ -345,7 +345,6 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
+15 -15
View File
@@ -187,7 +187,7 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
ScopedRelease releaseC {session, "ReleaseC"}; ScopedRelease releaseC {session, "ReleaseC"};
ScopedUser user {session, "MyUser"}; 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()}; auto transaction {session.createSharedTransaction()};
@@ -211,9 +211,9 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, trackA.get(), trackList.get()); session.create<TrackListEntry>(trackA.get(), trackList.get());
TrackListEntry::create(session, trackB.get(), trackList.get()); session.create<TrackListEntry>(trackB.get(), trackList.get());
TrackListEntry::create(session, trackB.get(), trackList.get()); session.create<TrackListEntry>(trackB.get(), trackList.get());
} }
{ {
@@ -249,9 +249,9 @@ TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, trackC.get(), trackList.get()); session.create<TrackListEntry>(trackC.get(), trackList.get());
TrackListEntry::create(session, trackC.get(), trackList.get()); session.create<TrackListEntry>(trackC.get(), trackList.get());
TrackListEntry::create(session, trackC.get(), trackList.get()); session.create<TrackListEntry>(trackC.get(), trackList.get());
} }
{ {
@@ -618,7 +618,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster) TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
{ {
ScopedUser user {session, "MyUser"}; 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"}; ScopedClusterType clusterType {session, "MyClusterType"};
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
@@ -630,7 +630,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
if (i < 5) if (i < 5)
TrackListEntry::create(session, tracks.back().get(), trackList.get()); session.create<TrackListEntry>(tracks.back().get(), trackList.get());
if (i < 10) if (i < 10)
cluster.get().modify()->addTrack(tracks.back().get()); cluster.get().modify()->addTrack(tracks.back().get());
@@ -650,7 +650,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
{ {
ScopedUser user {session, "MyUser"}; 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"}; ScopedClusterType clusterType {session, "MyClusterType"};
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"}; ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"}; ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
@@ -663,7 +663,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
if (i < 5) if (i < 5)
TrackListEntry::create(session, tracks.back().get(), trackList.get()); session.create<TrackListEntry>(tracks.back().get(), trackList.get());
if (i < 10) if (i < 10)
{ {
@@ -702,7 +702,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
{ {
ScopedUser user {session, "MyUser"}; 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"}; ScopedClusterType clusterType {session, "MyClusterType"};
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"}; ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"}; ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
@@ -742,7 +742,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
{ {
auto transaction {session.createUniqueTransaction()}; 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()}; 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()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2)); session.create<TrackListEntry>(track1.get(), trackList.get(), now.addSecs(2));
} }
{ {
+5 -2
View File
@@ -29,6 +29,7 @@
#include "services/database/Db.hpp" #include "services/database/Db.hpp"
#include "services/database/Listen.hpp" #include "services/database/Listen.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
#include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp" #include "services/database/TrackArtistLink.hpp"
@@ -50,7 +51,7 @@ class ScopedEntity
{ {
auto transaction {_session.createUniqueTransaction()}; 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); EXPECT_TRUE(entity);
_id = entity->getId(); _id = entity->getId();
} }
@@ -60,7 +61,9 @@ class ScopedEntity
auto transaction {_session.createUniqueTransaction()}; auto transaction {_session.createUniqueTransaction()};
auto entity {T::find(_session, _id)}; 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; ScopedEntity(const ScopedEntity&) = delete;
+134 -20
View File
@@ -31,7 +31,7 @@ TEST_F(DatabaseFixture, SingleTrackList)
EXPECT_EQ(TrackList::getCount(session), 0); 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()}; auto transaction {session.createSharedTransaction()};
@@ -39,10 +39,92 @@ 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()};
session.create<TrackListEntry>(track.get(), trackList1.get());
}
{
auto transaction {session.createSharedTransaction()};
const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))};
ASSERT_EQ(trackLists.results.size(), 2);
EXPECT_EQ(trackLists.results[0], trackList1.getId());
EXPECT_EQ(trackLists.results[1], trackList2.getId());
}
{
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track.get(), trackList2.get());
}
{
auto transaction {session.createSharedTransaction()};
const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))};
ASSERT_EQ(trackLists.results.size(), 2);
EXPECT_EQ(trackLists.results[0], trackList2.getId());
EXPECT_EQ(trackLists.results[1], trackList1.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
{ {
ScopedUser user {session, "MyUser"}; 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; std::list<ScopedTrack> tracks;
for (std::size_t i {}; i < 10; ++i) for (std::size_t i {}; i < 10; ++i)
@@ -50,7 +132,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session, "MyTrack" + std::to_string(i));
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, tracks.back().get(), trackList.get()); session.create<TrackListEntry>(tracks.back().get(), trackList.get());
} }
{ {
@@ -84,7 +166,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
{ {
ScopedUser user {session, "MyUser"}; 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 track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"}; ScopedTrack track2 {session, "MyTrack2"};
ScopedTrack track3 {session, "MyTrack3"}; ScopedTrack track3 {session, "MyTrack3"};
@@ -92,9 +174,9 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
{ {
Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now); session.create<TrackListEntry>(track1.get(), trackList.get(), now);
TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(-1)); session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(-1));
TrackListEntry::create(session, track3.get(), trackList.get(), now.addSecs(1)); session.create<TrackListEntry>(track3.get(), trackList.get(), now.addSecs(1));
} }
{ {
@@ -111,7 +193,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{ {
ScopedUser user {session, "MyUser"}; 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 track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack1"}; ScopedTrack track2 {session, "MyTrack1"};
ScopedArtist artist1 {session, "MyArtist1"}; ScopedArtist artist1 {session, "MyArtist1"};
@@ -141,7 +223,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get(), now); session.create<TrackListEntry>(track1.get(), trackList.get(), now);
} }
{ {
@@ -163,7 +245,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{ {
auto transaction {session.createUniqueTransaction()}; 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 +271,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{ {
auto transaction {session.createUniqueTransaction()}; 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 +298,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
TEST_F(DatabaseFixture, SingleTrackList_getArtists) TEST_F(DatabaseFixture, SingleTrackList_getArtists)
{ {
ScopedUser user {session, "MyUser"}; 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 track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"}; ScopedTrack track2 {session, "MyTrack2"};
ScopedRelease release {session, "MyRelease"}; ScopedRelease release {session, "MyRelease"};
@@ -235,7 +317,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); 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 +350,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); 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"); artist1.get().modify()->setSortName("ZZZ");
artist2.get().modify()->setSortName("AAA"); artist2.get().modify()->setSortName("AAA");
} }
@@ -297,7 +379,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getArtists)
TEST_F(DatabaseFixture, SingleTrackList_getReleases) TEST_F(DatabaseFixture, SingleTrackList_getReleases)
{ {
ScopedUser user {session, "MyUser"}; 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"}; ScopedTrack track {session, "MyTrack"};
ScopedRelease release {session, "MyRelease"}; ScopedRelease release {session, "MyRelease"};
@@ -315,7 +397,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getReleases)
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
TrackListEntry::create(session, track.get(), trackList.get()); session.create<TrackListEntry>(track.get(), trackList.get());
} }
{ {
@@ -332,7 +414,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getReleases)
TEST_F(DatabaseFixture, SingleTrackList_getTracks) TEST_F(DatabaseFixture, SingleTrackList_getTracks)
{ {
ScopedUser user {session, "MyUser"}; 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 track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack1"}; ScopedTrack track2 {session, "MyTrack1"};
@@ -347,9 +429,9 @@ TEST_F(DatabaseFixture, SingleTrackList_getTracks)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
TrackListEntry::create(session, track1.get(), trackList.get()); session.create<TrackListEntry>(track1.get(), trackList.get());
TrackListEntry::create(session, track2.get(), trackList.get()); session.create<TrackListEntry>(track2.get(), trackList.get());
TrackListEntry::create(session, track1.get(), trackList.get()); session.create<TrackListEntry>(track1.get(), trackList.get());
} }
{ {
@@ -363,3 +445,35 @@ TEST_F(DatabaseFixture, SingleTrackList_getTracks)
ASSERT_EQ(tracks[1]->getId(), track2->getId()); 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 Artist::pointer
createArtist(Session& session, const MetaData::Artist& artistInfo) 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) if (artistInfo.musicBrainzArtistID)
artist.modify()->setMBID(*artistInfo.musicBrainzArtistID); artist.modify()->setMBID(*artistInfo.musicBrainzArtistID);
@@ -187,7 +187,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album)
release = Release::find(session, *album.musicBrainzAlbumID); release = Release::find(session, *album.musicBrainzAlbumID);
if (!release) if (!release)
{ {
release = Release::create(session, album.name, album.musicBrainzAlbumID); release = session.create<Release>(album.name, album.musicBrainzAlbumID);
} }
else if (release->getName() != album.name) 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 // No release found with the same name and without MBID -> creating
if (!release) if (!release)
release = Release::create(session, album.name); release = session.create<Release>(album.name);
return release; return release;
} }
@@ -236,7 +236,7 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
{ {
auto cluster = clusterType->getCluster(clusterName); auto cluster = clusterType->getCluster(clusterName);
if (!cluster) if (!cluster)
cluster = Cluster::create(session, clusterType, clusterName); cluster = session.create<Cluster>(clusterType, clusterName);
clusters.push_back(cluster); clusters.push_back(cluster);
} }
@@ -567,7 +567,7 @@ ScannerService::fetchTrackFeatures(TrackId trackId, const UUID& recordingMBID)
if (!track) if (!track)
return false; return false;
TrackFeatures::create(_dbSession, track, data); _dbSession.create<TrackFeatures>(track, data);
} }
return true; return true;
@@ -788,7 +788,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
if (!track) if (!track)
{ {
// Create a new song // Create a new song
track = Track::create(_dbSession, file); track = _dbSession.create<Track>(file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'"; LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'";
stats.additions++; stats.additions++;
} }
@@ -51,7 +51,7 @@ namespace Scrobbling
if (!user) if (!user)
return; return;
starredObj = StarredObjType::create(session, obj, user, *scrobbler); starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
} }
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
starredObjId = starredObj->getId(); starredObjId = starredObj->getId();
@@ -88,7 +88,7 @@ namespace Scrobbling
if (!track) if (!track)
return; 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); dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
} }
@@ -574,7 +574,7 @@ namespace Scrobbling::ListenBrainz
if (!user) if (!user)
return; return;
StarredTrack::pointer starredTrack {StarredTrack::create(session, track, user, Database::Scrobbler::ListenBrainz)}; StarredTrack::pointer starredTrack {session.create<StarredTrack>(track, user, Database::Scrobbler::ListenBrainz)};
starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized); starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized);
context.importedFeedbackCount++; context.importedFeedbackCount++;
@@ -366,7 +366,7 @@ namespace Scrobbling::ListenBrainz
if (!track) if (!track)
return false; 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); dbListen.modify()->setScrobblingState(scrobblingState);
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
+13 -9
View File
@@ -553,7 +553,7 @@ handleCreatePlaylistRequest(RequestContext& context)
tracklist = TrackList::find(context.dbSession, *id); tracklist = TrackList::find(context.dbSession, *id);
if (!tracklist if (!tracklist
|| tracklist->getUser() != user || tracklist->getUser() != user
|| tracklist->getType() != TrackList::Type::Playlist) || tracklist->getType() != TrackListType::Playlist)
{ {
throw RequestedDataNotFoundError {}; throw RequestedDataNotFoundError {};
} }
@@ -563,7 +563,7 @@ handleCreatePlaylistRequest(RequestContext& context)
} }
else 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) for (const TrackId trackId : trackIds)
@@ -572,7 +572,7 @@ handleCreatePlaylistRequest(RequestContext& context)
if (!track) if (!track)
continue; continue;
TrackListEntry::create(context.dbSession, track, tracklist); context.dbSession.create<TrackListEntry>(track, tracklist);
} }
return Response::createOkResponse(context.serverProtocolVersion); return Response::createOkResponse(context.serverProtocolVersion);
@@ -594,7 +594,7 @@ handleCreateUserRequest(RequestContext& context)
if (user) if (user)
throw UserAlreadyExistsGenericError {}; throw UserAlreadyExistsGenericError {};
user = User::create(context.dbSession, username); user = context.dbSession.create<User>(username);
userId = user->getId(); userId = user->getId();
} }
@@ -644,7 +644,7 @@ handleDeletePlaylistRequest(RequestContext& context)
TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; TrackList::pointer tracklist {TrackList::find(context.dbSession, id)};
if (!tracklist if (!tracklist
|| tracklist->getUser() != user || tracklist->getUser() != user
|| tracklist->getType() != TrackList::Type::Playlist) || tracklist->getType() != TrackListType::Playlist)
{ {
throw RequestedDataNotFoundError {}; throw RequestedDataNotFoundError {};
} }
@@ -1321,7 +1321,11 @@ handleGetPlaylistsRequest(RequestContext& context)
Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& playlistsNode {response.createNode("playlists")}; 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) for (const TrackListId trackListId : tracklistIds.results)
{ {
const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)}; const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)};
@@ -1665,7 +1669,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; TrackList::pointer tracklist {TrackList::find(context.dbSession, id)};
if (!tracklist if (!tracklist
|| tracklist->getUser() != user || tracklist->getUser() != user
|| tracklist->getType() != TrackList::Type::Playlist) || tracklist->getType() != TrackListType::Playlist)
{ {
throw RequestedDataNotFoundError {}; throw RequestedDataNotFoundError {};
} }
@@ -1695,7 +1699,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
if (!track) if (!track)
continue; continue;
TrackListEntry::create(context.dbSession, track, tracklist); context.dbSession.create<TrackListEntry>(track, tracklist);
} }
return Response::createOkResponse(context.serverProtocolVersion); return Response::createOkResponse(context.serverProtocolVersion);
@@ -1750,7 +1754,7 @@ handleCreateBookmark(RequestContext& context)
// Replace any existing bookmark // Replace any existing bookmark
auto bookmark {TrackBookmark::find(context.dbSession, user->getId(), trackId)}; auto bookmark {TrackBookmark::find(context.dbSession, user->getId(), trackId)};
if (!bookmark) if (!bookmark)
bookmark = TrackBookmark::create(context.dbSession, user, track); bookmark = context.dbSession.create<TrackBookmark>(user, track);
bookmark.modify()->setOffset(std::chrono::milliseconds {position}); bookmark.modify()->setOffset(std::chrono::milliseconds {position});
if (comment) if (comment)
+1 -1
View File
@@ -164,7 +164,7 @@ bufferToString(const std::vector<unsigned char>& data)
} }
std::string 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}; std::string res {str};
size_t pos = 0; size_t pos = 0;
+1 -1
View File
@@ -87,7 +87,7 @@ readAs(std::string_view str);
[[nodiscard]] [[nodiscard]]
std::string 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]] [[nodiscard]]
std::string std::string
+2
View File
@@ -39,6 +39,8 @@ add_executable(lms
ui/explore/SearchView.cpp ui/explore/SearchView.cpp
ui/explore/TrackCollector.cpp ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp ui/explore/TrackListHelpers.cpp
ui/explore/TrackListView.cpp
ui/explore/TrackListsView.cpp
ui/explore/TracksView.cpp ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodeResource.cpp ui/resource/AudioTranscodeResource.cpp
+24 -2
View File
@@ -34,6 +34,7 @@
#include "services/database/Db.hpp" #include "services/database/Db.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/TrackList.hpp"
#include "services/database/User.hpp" #include "services/database/User.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -191,6 +192,8 @@ LmsApplication::init()
messageResourceBundle().use(appRoot() + "releases"); messageResourceBundle().use(appRoot() + "releases");
messageResourceBundle().use(appRoot() + "search"); messageResourceBundle().use(appRoot() + "search");
messageResourceBundle().use(appRoot() + "settings"); messageResourceBundle().use(appRoot() + "settings");
messageResourceBundle().use(appRoot() + "tracklist");
messageResourceBundle().use(appRoot() + "tracklists");
messageResourceBundle().use(appRoot() + "tracks"); messageResourceBundle().use(appRoot() + "tracks");
// Handle Media Scanner events and other session events // Handle Media Scanner events and other session events
@@ -299,6 +302,23 @@ LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool add
return res; 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 void
LmsApplication::handleException(LmsApplicationException& e) LmsApplication::handleException(LmsApplicationException& e)
{ {
@@ -348,6 +368,8 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
{ "/release", IdxExplore, false }, { "/release", IdxExplore, false },
{ "/search", IdxExplore, false }, { "/search", IdxExplore, false },
{ "/tracks", IdxExplore, false }, { "/tracks", IdxExplore, false },
{ "/tracklists", IdxExplore, false },
{ "/tracklist", IdxExplore, false },
{ "/playqueue", IdxPlayQueue, false }, { "/playqueue", IdxPlayQueue, false },
{ "/settings", IdxSettings, false }, { "/settings", IdxSettings, false },
{ "/admin/database", IdxAdminDatabase, true }, { "/admin/database", IdxAdminDatabase, true },
@@ -432,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>("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>("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>("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")}; 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->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings")); navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
@@ -544,7 +566,7 @@ LmsApplication::createHome()
}); });
} }
internalPathChanged().connect([=] internalPathChanged().connect(mainStack, [=]
{ {
handlePathChange(*mainStack, isAdmin); handlePathChange(*mainStack, isAdmin);
}); });
+2
View File
@@ -36,6 +36,7 @@ namespace Database
class Db; class Db;
class Release; class Release;
class Session; class Session;
class TrackList;
class User; 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 std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release); 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> 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; } MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
PlayQueue& getPlayQueue() const { return *_playQueue; } PlayQueue& getPlayQueue() const { return *_playQueue; }
+6
View File
@@ -42,6 +42,12 @@ namespace UserInterface
ReleaseNotFoundException() : LmsApplicationException {Wt::WString::tr("Lms.Error.release-not-found")} {} 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 class UserNotFoundException : public LmsApplicationException
{ {
public: public:
+7
View File
@@ -22,6 +22,7 @@
#include <Wt/Json/Object.h> #include <Wt/Json/Object.h>
#include <Wt/Json/Value.h> #include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h> #include <Wt/Json/Serializer.h>
#include <Wt/WPushButton.h>
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -209,6 +210,12 @@ MediaPlayer::MediaPlayer()
_artist = bindNew<Wt::WAnchor>("artist"); _artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release"); _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) _settingsLoaded.connect([this](const std::string& settings)
{ {
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'"; LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
+222 -12
View File
@@ -20,7 +20,13 @@
#include "PlayQueue.hpp" #include "PlayQueue.hpp"
#include <Wt/WCheckBox.h> #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/WPushButton.h>
#include <Wt/WStackedWidget.h>
#include <Wt/WTemplateFormView.h>
#include "services/database/Cluster.hpp" #include "services/database/Cluster.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
@@ -36,25 +42,97 @@
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/InfiniteScrollingContainer.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/ValueStringModel.hpp"
#include "resource/DownloadResource.hpp" #include "resource/DownloadResource.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "MediaPlayer.hpp" #include "MediaPlayer.hpp"
#include "ModalManager.hpp"
#include "Utils.hpp" #include "Utils.hpp"
namespace UserInterface { 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() PlayQueue::PlayQueue()
: Template {Wt::WString::tr("Lms.PlayQueue.template")} : Template {Wt::WString::tr("Lms.PlayQueue.template")}
{ {
addFunction("id", &Wt::WTemplate::Functions::id); addFunction("id", &Wt::WTemplate::Functions::id);
addFunction("tr", &Wt::WTemplate::Functions::tr); 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([=] clearBtn->clicked().connect([=]
{ {
clearTracks(); 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 = bindNew<InfiniteScrollingContainer>("entries");
_entriesContainer->onRequestElements.connect([this] _entriesContainer->onRequestElements.connect([this]
{ {
@@ -62,7 +140,7 @@ PlayQueue::PlayQueue()
updateCurrentTrack(true); 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([=] shuffleBtn->clicked().connect([=]
{ {
{ {
@@ -74,7 +152,7 @@ PlayQueue::PlayQueue()
getTrackList().modify()->clear(); getTrackList().modify()->clear();
for (const auto& entry : entries) for (const auto& entry : entries)
Database::TrackListEntry::create(LmsApp->getDbSession(), entry->getTrack(), trackList); LmsApp->getDbSession().create<Database::TrackListEntry>(entry->getTrack(), trackList);
} }
_entriesContainer->clear(); _entriesContainer->clear();
addSome(); addSome();
@@ -108,7 +186,8 @@ PlayQueue::PlayQueue()
_radioBtn->setCheckState(Wt::CheckState::Checked); _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([=] LmsApp->preQuit().connect([=]
{ {
@@ -148,14 +227,14 @@ PlayQueue::PlayQueue()
}); });
static const std::string queuedListName {"__queued_tracks__"}; 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) 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 else
{ {
static const std::string currentPlayQueueName {"__current__playqueue__"}; 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(); _tracklistId = trackList->getId();
@@ -287,7 +366,9 @@ PlayQueue::updateInfo()
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; 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 void
@@ -323,7 +404,7 @@ PlayQueue::enqueueTracks(const std::vector<Database::TrackId>& trackIds)
if (nbTracksQueued == nbTracksToEnqueue) if (nbTracksQueued == nbTracksToEnqueue)
break; break;
Database::TrackListEntry::create(LmsApp->getDbSession(), track, tracklist); LmsApp->getDbSession().create<Database::TrackListEntry>(track, tracklist);
nbTracksQueued++; nbTracksQueued++;
} }
} }
@@ -420,7 +501,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); 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([=] playBtn->clicked().connect([=]
{ {
const std::optional<std::size_t> pos {_entriesContainer->getIndexOf(*entry)}; const std::optional<std::size_t> pos {_entriesContainer->getIndexOf(*entry)};
@@ -428,7 +509,8 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
loadTrack(*pos, true); 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([=] delBtn->clicked().connect([=]
{ {
// Remove the entry n both the widget tree and the playqueue // Remove the entry n both the widget tree and the playqueue
@@ -451,7 +533,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
updateInfo(); 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); }}; 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; 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 } // namespace UserInterface
+5
View File
@@ -89,6 +89,10 @@ class PlayQueue : public Template
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder); void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
void addRadioTrackFromClusters(); void addRadioTrackFromClusters();
std::optional<float> getReplayGain(std::size_t pos, const Database::ObjectPtr<Database::Track>& track) const; std::optional<float> getReplayGain(std::size_t pos, const Database::ObjectPtr<Database::Track>& track) const;
void saveAsTrackList();
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 _capacity {1000};
static inline constexpr std::size_t _batchSize {12}; static inline constexpr std::size_t _batchSize {12};
@@ -97,6 +101,7 @@ class PlayQueue : public Template
Database::TrackListId _tracklistId {}; Database::TrackListId _tracklistId {};
InfiniteScrollingContainer* _entriesContainer {}; InfiniteScrollingContainer* _entriesContainer {};
Wt::WText* _nbTracks {}; Wt::WText* _nbTracks {};
Wt::WText* _duration {};
Wt::WCheckBox* _repeatBtn {}; Wt::WCheckBox* _repeatBtn {};
Wt::WCheckBox* _radioBtn {}; Wt::WCheckBox* _radioBtn {};
std::optional<std::size_t> _trackPos; // current track position, if set std::optional<std::size_t> _trackPos; // current track position, if set
+2
View File
@@ -25,6 +25,7 @@
#include "services/database/ArtistId.hpp" #include "services/database/ArtistId.hpp"
#include "services/database/ReleaseId.hpp" #include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp" #include "services/database/TrackId.hpp"
#include "services/database/TrackListId.hpp"
namespace UserInterface namespace UserInterface
{ {
@@ -38,5 +39,6 @@ namespace UserInterface
using PlayQueueActionArtistSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ArtistId>&>; using PlayQueueActionArtistSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ArtistId>&>;
using PlayQueueActionReleaseSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ReleaseId>&>; using PlayQueueActionReleaseSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::ReleaseId>&>;
using PlayQueueActionTrackSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::TrackId>&>; using PlayQueueActionTrackSignal = Wt::Signal<PlayQueueAction, const std::vector<Database::TrackId>&>;
using PlayQueueActionTrackListSignal = Wt::Signal<PlayQueueAction, Database::TrackListId>;
} }
+15 -3
View File
@@ -37,9 +37,21 @@ namespace UserInterface::Utils
const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)}; const std::chrono::seconds duration {std::chrono::duration_cast<std::chrono::seconds>(msDuration)};
std::ostringstream oss; std::ostringstream oss;
oss << (duration.count() / 60)
<< ":" if (duration.count() >= 3600)
<< std::setfill('0') << std::setw(2) << duration.count() % 60; {
oss << (duration.count() / 3600)
<< ":"
<< std::setfill('0') << std::setw(2) << (duration.count() % 3600) / 60
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
else
{
oss << (duration.count() / 60)
<< ":"
<< std::setfill('0') << std::setw(2) << duration.count() % 60;
}
return oss.str(); return oss.str();
} }
+1 -1
View File
@@ -67,7 +67,7 @@ class InitWizardModel : public Wt::WFormModel
if (Database::User::getCount(LmsApp->getDbSession()) > 0) if (Database::User::getCount(LmsApp->getDbSession()) > 0)
throw LmsException {"Admin user already created"}; throw LmsException {"Admin user already created"};
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; Database::User::pointer user {LmsApp->getDbSession().create<Database::User>(valueText(AdminLoginField).toUTF8())};
user.modify()->setType(Database::UserType::ADMIN); user.modify()->setType(Database::UserType::ADMIN);
Service<::Auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8()); Service<::Auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8());
} }
+1 -1
View File
@@ -96,7 +96,7 @@ class UserModel : public Wt::WFormModel
throw UserNotAllowedException {}; throw UserNotAllowedException {};
// Create user // Create user
user = User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); user = LmsApp->getDbSession().create<User>(valueText(LoginField).toUTF8());
if (Wt::asNumber(value(DemoField))) if (Wt::asNumber(value(DemoField)))
user.modify()->setType(UserType::DEMO); user.modify()->setType(UserType::DEMO);
+11 -7
View File
@@ -93,27 +93,31 @@ UsersView::refreshView()
continue; continue;
entry->setCondition("if-edit", true); entry->setCondition("if-edit", true);
Wt::WPushButton* editBtn = entry->bindNew<Wt::WPushButton>("edit-btn", Wt::WString::tr("Lms.Admin.Users.edit")); Wt::WPushButton* editBtn = entry->bindNew<Wt::WPushButton>("edit-btn", Wt::WString::tr("Lms.template.edit-btn"), Wt::TextFormat::XHTML);
editBtn->setToolTip(Wt::WString::tr("Lms.edit"));
editBtn->clicked().connect([=]() editBtn->clicked().connect([=]()
{ {
LmsApp->setInternalPath("/admin/user/" + userId.toString(), true); LmsApp->setInternalPath("/admin/user/" + userId.toString(), true);
}); });
Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del")); 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([=] delBtn->clicked().connect([=]
{ {
auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.delete-user"))}; auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.delete-user"))};
modal->addFunction("tr", &Wt::WTemplate::Functions::tr); modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WWidget* modalPtr {modal.get()}; Wt::WWidget* modalPtr {modal.get()};
auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"))}; auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.delete"))};
delBtn->clicked().connect([=] delBtn->clicked().connect([=]
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
User::pointer user {User::find(LmsApp->getDbSession(), userId)}; User::pointer user {User::find(LmsApp->getDbSession(), userId)};
if (user) if (user)
user.remove(); user.remove();
}
_container->removeWidget(entry); _container->removeWidget(entry);
@@ -33,7 +33,7 @@ namespace UserInterface
{ {
public: public:
// "text" must contain loading-indicator and "elements" // "text" must contain loading-indicator and "elements"
InfiniteScrollingContainer(const Wt::WString& text = Wt::WString::tr("Lms.infinite-scrolling-container")); InfiniteScrollingContainer(const Wt::WString& text = Wt::WString::tr("Lms.infinite-scrolling-container.template"));
void clear(); void clear();
std::size_t getCount(); std::size_t getCount();
+46 -2
View File
@@ -37,6 +37,8 @@
#include "ReleasesView.hpp" #include "ReleasesView.hpp"
#include "ReleaseView.hpp" #include "ReleaseView.hpp"
#include "SearchView.hpp" #include "SearchView.hpp"
#include "TrackListView.hpp"
#include "TrackListsView.hpp"
#include "TracksView.hpp" #include "TracksView.hpp"
namespace UserInterface { namespace UserInterface {
@@ -50,6 +52,8 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
{ {
IdxArtists = 0, IdxArtists = 0,
IdxArtist, IdxArtist,
IdxTrackLists,
IdxTrackList,
IdxReleases, IdxReleases,
IdxRelease, IdxRelease,
IdxSearch, IdxSearch,
@@ -60,6 +64,8 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
{ {
{ "/artists", IdxArtists }, { "/artists", IdxArtists },
{ "/artist", IdxArtist }, { "/artist", IdxArtist },
{ "/tracklists", IdxTrackLists },
{ "/tracklist", IdxTrackList },
{ "/releases", IdxReleases }, { "/releases", IdxReleases },
{ "/release", IdxRelease }, { "/release", IdxRelease },
{ "/search", IdxSearch }, { "/search", IdxSearch },
@@ -88,6 +94,7 @@ Explore::Explore(Filters* filters)
Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")}; Wt::WStackedWidget* contentsStack {bindNew<Wt::WStackedWidget>("contents")};
contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default contentsStack->setOverflow(Wt::Overflow::Visible); // wt makes it hidden by default
// same order as enum Idx
auto artists = std::make_unique<Artists>(*_filters); auto artists = std::make_unique<Artists>(*_filters);
contentsStack->addWidget(std::move(artists)); contentsStack->addWidget(std::move(artists));
@@ -96,6 +103,19 @@ Explore::Explore(Filters* filters)
artist->tracksAction.connect(this, &Explore::handleTracksAction); artist->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(artist)); contentsStack->addWidget(std::move(artist));
auto trackLists {std::make_unique<TrackLists>(*_filters)};
TrackLists* trackListsPtr {trackLists.get()};
trackLists->trackListAction.connect(this, &Explore::handleTrackListAction);
contentsStack->addWidget(std::move(trackLists));
auto trackList {std::make_unique<TrackList>(*_filters)};
TrackList* trackListPtr {trackList.get()};
trackList->trackListAction.connect(this, &Explore::handleTrackListAction);
trackList->tracksAction.connect(this, &Explore::handleTracksAction);
contentsStack->addWidget(std::move(trackList));
trackListPtr->trackListDeleted.connect(trackListsPtr, &TrackLists::onTrackListDeleted);
auto releases = std::make_unique<Releases>(*_filters); auto releases = std::make_unique<Releases>(*_filters);
releases->releasesAction.connect(this, &Explore::handleReleasesAction); releases->releasesAction.connect(this, &Explore::handleReleasesAction);
contentsStack->addWidget(std::move(releases)); contentsStack->addWidget(std::move(releases));
@@ -163,7 +183,6 @@ std::vector<Database::TrackId>
getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount) getReleasesTracks(Database::Session& session, const std::vector<Database::ReleaseId>& releasesId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
{ {
using namespace Database; using namespace Database;
assert(maxTrackCount); assert(maxTrackCount);
std::vector<TrackId> res; std::vector<TrackId> res;
@@ -190,6 +209,25 @@ getReleasesTracks(Database::Session& session, const std::vector<Database::Releas
return res; return res;
} }
static
std::vector<Database::TrackId>
getTrackListTracks(Database::Session& session, Database::TrackListId trackListId, const std::vector<Database::ClusterId>& clusters, std::size_t maxTrackCount)
{
using namespace Database;
assert(maxTrackCount);
auto transaction {session.createSharedTransaction()};
Database::Track::FindParameters params;
params.setTrackList(trackListId);
params.setClusters(clusters);
params.setRange({0, maxTrackCount});
params.setSortMethod(TrackSortMethod::TrackList);
params.setDistinct(false);
return Database::Track::find(session, params).results;
}
void void
Explore::handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId) Explore::handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId)
{ {
@@ -202,12 +240,18 @@ Explore::handleReleasesAction(PlayQueueAction action, const std::vector<Database
tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds(), _maxTrackCount)); tracksAction.emit(action, getReleasesTracks(LmsApp->getDbSession(), releasesId, _filters->getClusterIds(), _maxTrackCount));
} }
void
Explore::handleTrackListAction(PlayQueueAction action, Database::TrackListId trackListId)
{
tracksAction.emit(action, getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters->getClusterIds(), _maxTrackCount));
}
void void
Explore::handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId) Explore::handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId)
{ {
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
tracksAction.emit(action, tracksId); tracksAction.emit(action, tracksId);
} }
} // namespace UserInterface } // namespace UserInterface
+1
View File
@@ -42,6 +42,7 @@ namespace UserInterface
private: private:
void handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId); void handleArtistsAction(PlayQueueAction action, const std::vector<Database::ArtistId>& artistsId);
void handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId); void handleReleasesAction(PlayQueueAction action, const std::vector<Database::ReleaseId>& releasesId);
void handleTrackListAction(PlayQueueAction action, Database::TrackListId trackiListId);
void handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId); void handleTracksAction(PlayQueueAction action, const std::vector<Database::TrackId>& tracksId);
Filters* _filters {}; Filters* _filters {};
+2 -2
View File
@@ -279,14 +279,14 @@ Release::refreshView()
entry->bindInt("track-number", *trackNumber); entry->bindInt("track-number", *trackNumber);
} }
Wt::WPushButton* playBtn {entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.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([=] playBtn->clicked().connect([=]
{ {
tracksAction.emit(PlayQueueAction::Play, {trackId}); tracksAction.emit(PlayQueueAction::Play, {trackId});
}); });
{ {
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last")) entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=] ->clicked().connect([=]
{ {
-2
View File
@@ -19,8 +19,6 @@
#pragma once #pragma once
#include <Wt/WContainerWidget.h>
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "common/Template.hpp" #include "common/Template.hpp"
+1 -1
View File
@@ -50,7 +50,7 @@ namespace UserInterface
_releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); _releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
_releases->onRequestElements.connect([this] { addSomeReleases(); }); _releases->onRequestElements.connect([this] { addSomeReleases(); });
_tracks = bindNew<InfiniteScrollingContainer>("tracks", Wt::WString::tr("Lms.infinite-scrolling-container")); _tracks = bindNew<InfiniteScrollingContainer>("tracks");
_tracks->onRequestElements.connect([this] { addSomeTracks(); }); _tracks->onRequestElements.connect([this] { addSomeTracks(); });
_filters->updated().connect([=] _filters->updated().connect([=]
+2 -3
View File
@@ -23,7 +23,6 @@
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WText.h>
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
@@ -74,13 +73,13 @@ namespace UserInterface::TrackListHelpers
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.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([trackId, &tracksAction] playBtn->clicked().connect([trackId, &tracksAction]
{ {
tracksAction.emit(PlayQueueAction::Play, {trackId}); tracksAction.emit(PlayQueueAction::Play, {trackId});
}); });
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML); entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last")) entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked().connect([=, &tracksAction] ->clicked().connect([=, &tracksAction]
{ {
+205
View File
@@ -0,0 +1,205 @@
/*
* 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/>.
*/
#include "TrackListView.hpp"
#include <Wt/WPushButton.h>
#include "services/database/Cluster.hpp"
#include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "resource/DownloadResource.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "LmsApplicationException.hpp"
#include "ModalManager.hpp"
#include "TrackListHelpers.hpp"
#include "Utils.hpp"
using namespace Database;
namespace
{
static
std::optional<TrackListId>
extractTrackListIdFromInternalPath()
{
return StringUtils::readAs<TrackListId::ValueType>(wApp->internalPathNextPart("/tracklist/"));
}
}
namespace UserInterface
{
TrackList::TrackList(Filters& filters)
: Template {Wt::WString::tr("Lms.Explore.TrackList.template")}
, _filters {filters}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
wApp->internalPathChanged().connect(this, [this]
{
refreshView();
});
_filters.updated().connect([this]
{
refreshView();
});
refreshView();
}
void
TrackList::refreshView()
{
if (!wApp->internalPathMatches("/tracklist/"))
return;
const std::optional<TrackListId> trackListId {extractTrackListIdFromInternalPath()};
if (!trackListId)
throw TrackListNotFoundException {};
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)};
if (!trackList)
throw TrackListNotFoundException {};
_trackListId = *trackListId;
clear();
bindString("name", std::string {trackList->getName()}, Wt::TextFormat::Plain);
bindString("duration", Utils::durationToString(trackList->getDuration()));
const auto trackCount {trackList->getCount()};
bindString("track-count", Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount));
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
{
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
const auto clusterGroups {trackList->getClusterGroups(clusterTypes, 3)};
for (const auto& clusters : clusterGroups)
{
for (const Database::Cluster::pointer& cluster : clusters)
{
const ClusterId clusterId {cluster->getId()};
Wt::WInteractWidget* entry {clusterContainers->addWidget(Utils::createCluster(clusterId))};
entry->clicked().connect([=]
{
_filters.add(clusterId);
});
}
}
}
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
->clicked().connect([=]
{
trackListAction.emit(PlayQueueAction::Play, *trackListId);
});
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
->clicked().connect([=]
{
trackListAction.emit(PlayQueueAction::PlayShuffled, *trackListId);
});
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
->clicked().connect([=]
{
trackListAction.emit(PlayQueueAction::PlayLast, *trackListId);
});
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
->setLink(Wt::WLink {std::make_unique<DownloadTrackListResource>(*trackListId)});
bindNew<Wt::WPushButton>("delete", Wt::WString::tr("Lms.delete"))
->clicked().connect([=]
{
auto modal {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.TrackList.template.delete-tracklist"))};
modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WWidget* modalPtr {modal.get()};
auto* delBtn {modal->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.delete"))};
delBtn->clicked().connect([=]
{
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), *trackListId)};
if (trackList)
trackList.remove();
}
clear();
trackListDeleted.emit(*trackListId);
LmsApp->setInternalPath("/tracklists", true);
LmsApp->getModalManager().dispose(modalPtr);
});
auto* cancelBtn {modal->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"))};
cancelBtn->clicked().connect([=]
{
LmsApp->getModalManager().dispose(modalPtr);
});
LmsApp->getModalManager().show(std::move(modal));
});
_container = bindNew<InfiniteScrollingContainer>("tracks");
_container->onRequestElements.connect([this]
{
addSome();
});
addSome();
}
void
TrackList::addSome()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Database::Track::FindParameters params;
params.setClusters(_filters.getClusterIds());
params.setTrackList(_trackListId);
params.setSortMethod(Database::TrackSortMethod::TrackList);
params.setRange({static_cast<std::size_t>(_container->getCount()), _batchSize});
params.setDistinct(false);
const auto trackIds {Database::Track::find(LmsApp->getDbSession(), params)};
for (const TrackId trackId : trackIds.results)
{
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
_container->add(TrackListHelpers::createEntry(track, tracksAction));
}
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
}
} // namespace UserInterface
+55
View File
@@ -0,0 +1,55 @@
/*
* 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/TrackListId.hpp"
#include "services/database/Types.hpp"
#include "common/Template.hpp"
#include "PlayQueueAction.hpp"
namespace UserInterface
{
class Filters;
class InfiniteScrollingContainer;
class TrackList : public Template
{
public:
TrackList(Filters& filters);
PlayQueueActionTrackListSignal trackListAction;
PlayQueueActionTrackSignal tracksAction;
Wt::Signal<Database::TrackListId> trackListDeleted;
private:
void refreshView();
void addSome();
static constexpr std::size_t _batchSize {6};
static constexpr std::size_t _maxCount {8000};
Filters& _filters;
Database::TrackListId _trackListId;
InfiniteScrollingContainer* _container {};
};
} // namespace UserInterface
+146
View File
@@ -0,0 +1,146 @@
/*
* 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/>.
*/
#include "TrackListsView.hpp"
#include <Wt/WPushButton.h>
#include "services/database/Session.hpp"
#include "services/database/TrackList.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "common/Template.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "Utils.hpp"
using namespace Database;
namespace UserInterface
{
TrackLists::TrackLists(Filters& filters)
: Template {Wt::WString::tr("Lms.Explore.TrackLists.template")}
, _filters {filters}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
auto bindMenuItem {[this](const std::string& var, const Wt::WString& title, Mode mode)
{
auto *menuItem {bindNew<Wt::WPushButton>(var, title)};
menuItem->clicked().connect([=]
{
_mode = mode;
refreshView();
_currentActiveItem->removeStyleClass("active");
menuItem->addStyleClass("active");
_currentActiveItem = menuItem;
});
if (mode == _mode)
{
_currentActiveItem = menuItem;
_currentActiveItem->addStyleClass("active");
}
}};
bindMenuItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), Mode::RecentlyModified);
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), Mode::All);
_container = bindNew<InfiniteScrollingContainer>("tracklists", Wt::WString::tr("Lms.Explore.TrackLists.template.container"));
_container->onRequestElements.connect([this]
{
addSome();
});
_filters.updated().connect([this]
{
refreshView();
});
refreshView();
}
void
TrackLists::onTrackListDeleted(Database::TrackListId trackListId)
{
auto itTrackList {_trackListWidgets.find(trackListId)};
if (itTrackList == std::end(_trackListWidgets))
return;
_container->remove(*itTrackList->second);
_trackListWidgets.erase(itTrackList);
}
void
TrackLists::refreshView()
{
_container->clear();
_trackListWidgets.clear();
addSome();
}
void
TrackLists::addSome()
{
const Range range {static_cast<std::size_t>(_container->getCount()), _batchSize};
Session& session {LmsApp->getDbSession()};
auto transaction {session.createSharedTransaction()};
TrackList::FindParameters params;
params.setClusters(_filters.getClusterIds());
params.setUser(LmsApp->getUserId());
params.setType(TrackListType::Playlist);
params.setRange(range);
switch (_mode)
{
case Mode::All:
params.setSortMethod(TrackListSortMethod::Name);
break;
case Mode::RecentlyModified:
params.setSortMethod(TrackListSortMethod::LastModifiedDesc);
break;
}
const auto trackListIds {TrackList::find(session, params)};
for (const TrackListId trackListId : trackListIds.results)
{
if (const TrackList::pointer trackList {TrackList::find(LmsApp->getDbSession(), trackListId)})
addTracklist(trackList);
}
_container->setHasMore(trackListIds.moreResults && _container->getCount() <= _maxCount);
}
void
TrackLists::addTracklist(const ObjectPtr<TrackList>& trackList)
{
const TrackListId trackListId {trackList->getId()};
WTemplate* entry {_container->addNew<Template>(Wt::WString::tr("Lms.Explore.TrackLists.template.entry"))};
entry->bindWidget("name", LmsApplication::createTrackListAnchor(trackList));
assert(_trackListWidgets.find(trackListId) == std::cend(_trackListWidgets));
_trackListWidgets.emplace(trackListId, entry);
}
} // namespace UserInterface
+72
View File
@@ -0,0 +1,72 @@
/*
* 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 <unordered_map>
#include <Wt/WTemplate.h>
#include "services/database/Object.hpp"
#include "services/database/TrackListId.hpp"
#include "services/database/Types.hpp"
#include "common/Template.hpp"
#include "PlayQueueAction.hpp"
namespace Database
{
class TrackList;
}
namespace UserInterface
{
class Filters;
class InfiniteScrollingContainer;
class TrackLists : public Template
{
public:
TrackLists(Filters& filters);
PlayQueueActionTrackListSignal trackListAction;
void onTrackListDeleted(Database::TrackListId trackListId);
private:
enum class Mode
{
RecentlyModified,
All,
};
void refreshView();
void addSome();
void addTracklist(const Database::ObjectPtr<Database::TrackList>& trackList);
static constexpr std::size_t _batchSize {30};
static constexpr std::size_t _maxCount {500};
Mode _mode {Mode::RecentlyModified};
Filters& _filters;
Wt::WWidget* _currentActiveItem {};
InfiniteScrollingContainer* _container {};
std::unordered_map<Database::TrackListId, Wt::WWidget*> _trackListWidgets;
};
} // namespace UserInterface
+37 -1
View File
@@ -30,6 +30,7 @@
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Zipper.hpp" #include "utils/Zipper.hpp"
@@ -59,7 +60,8 @@ DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Resp
else else
{ {
zipper = createZipper(); zipper = createZipper();
response.setContentLength(zipper->getTotalZipFile()); if (zipper)
response.setContentLength(zipper->getTotalZipFile());
response.setMimeType("application/zip"); response.setMimeType("application/zip");
} }
@@ -141,6 +143,13 @@ getTrackPathName(Database::Track::pointer track)
return fileName.str(); return fileName.str();
} }
static
std::string
getTrackListPathName(Database::TrackList::pointer trackList)
{
return StringUtils::replaceInString(trackList->getName(), "/", "_");
}
static static
std::unique_ptr<Zip::Zipper> std::unique_ptr<Zip::Zipper>
createZipper(const std::vector<Database::Track::pointer>& tracks) createZipper(const std::vector<Database::Track::pointer>& tracks)
@@ -250,4 +259,31 @@ DownloadTrackResource::createZipper()
return UserInterface::createZipper({track}); return UserInterface::createZipper({track});
} }
DownloadTrackListResource::DownloadTrackListResource(Database::TrackListId trackListId)
: _trackListId {trackListId}
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::TrackList::pointer trackList {Database::TrackList::find(LmsApp->getDbSession(), trackListId)};
if (trackList)
suggestFileName(getTrackListPathName(trackList) + ".zip");
}
std::unique_ptr<Zip::Zipper>
DownloadTrackListResource::createZipper()
{
using namespace Database;
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
Track::FindParameters params;
params.setTrackList(_trackListId);
const auto trackResults {Track::find(LmsApp->getDbSession(), params)};
std::vector<Track::pointer> tracks;
tracks.reserve(trackResults.results.size());
std::transform(std::cbegin(trackResults.results), std::cend(trackResults.results), std::back_inserter(tracks), [](TrackId trackId){ return Track::find(LmsApp->getDbSession(), trackId); });
return UserInterface::createZipper(tracks);
}
} // namespace UserInterface } // namespace UserInterface
+11
View File
@@ -25,6 +25,7 @@
#include "services/database/ArtistId.hpp" #include "services/database/ArtistId.hpp"
#include "services/database/ReleaseId.hpp" #include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp" #include "services/database/TrackId.hpp"
#include "services/database/TrackListId.hpp"
#include "utils/Zipper.hpp" #include "utils/Zipper.hpp"
namespace UserInterface namespace UserInterface
@@ -70,5 +71,15 @@ namespace UserInterface
std::unique_ptr<Zip::Zipper> createZipper() override; std::unique_ptr<Zip::Zipper> createZipper() override;
Database::TrackId _trackId; Database::TrackId _trackId;
}; };
class DownloadTrackListResource : public DownloadResource
{
public:
DownloadTrackListResource(Database::TrackListId trackListId);
private:
std::unique_ptr<Zip::Zipper> createZipper() override;
Database::TrackListId _trackListId;
};
} // namespace UserInterface } // namespace UserInterface