New UI layout and UI improvements. Fixes #50
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Admin.Users.template">
|
||||
<div class="page-header">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Admin.Users.users}</h2>
|
||||
</div>
|
||||
${users}
|
||||
|
||||
+15
-5
@@ -3,12 +3,22 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Artist.template">
|
||||
<div class="page-header">
|
||||
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
||||
${clusters}
|
||||
${play-btn}${add-btn}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="Lms-header">
|
||||
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
||||
${clusters}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
${releases class="Lms-explore-artist-entries-container"}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||
${similar-artists}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${releases}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artist.template.entry">
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.ArtistInfo.template">
|
||||
<h3>${tr:Lms.Explore.ArtistInfo.similar-artists}</h3>
|
||||
${similar-artists}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.ArtistLink.template">
|
||||
<div class="row Lms-explore-artistlink Lms-entry-hoverable Lms-vertical-align">
|
||||
<div class="col-xs-12">
|
||||
${name class="Lms-artistname text-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+12
-11
@@ -3,21 +3,14 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Artists.template">
|
||||
<div class="page-header">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.artists}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-3">
|
||||
<div class="Lms-explore-artists-input">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
${link-type}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-9">
|
||||
<div class="input-group Lms-explore-artists-input">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${artists}
|
||||
<div class="row">
|
||||
@@ -35,4 +28,12 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry-small">
|
||||
<div class="row Lms-explore-artists-entry-small Lms-entry-hoverable Lms-vertical-align">
|
||||
<div class="col-xs-12">
|
||||
${name class="Lms-artistname text-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.ArtistsInfo.template">
|
||||
<h3>${tr:Lms.Explore.ArtistsInfo.most-played}</h3>
|
||||
${most-played}
|
||||
<h3>${tr:Lms.Explore.ArtistsInfo.recently-added}</h3>
|
||||
${recently-added}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+2
-11
@@ -2,23 +2,14 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.template">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
${contents class="Lms-explore-contents"}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
${filters class="well well-sm"}
|
||||
${info class="well well-sm"}
|
||||
</div>
|
||||
</div>
|
||||
${contents class="Lms-explore-contents"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.add-btn"><i class="fa fa-plus-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-play-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||
|
||||
<message id="Lms.Explore.template.filters">
|
||||
<h3>${tr:Lms.Explore.filters} ${add-filter class="btn-primary"}</h3>
|
||||
${clusters}
|
||||
${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.add-filter">
|
||||
|
||||
+16
-16
@@ -4,13 +4,13 @@
|
||||
|
||||
<!--Common-->
|
||||
<message id="Lms.add">Add</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Apply</message>
|
||||
<message id="Lms.discard">Discard</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.cancel">Cancel</message>
|
||||
<message id="Lms.create">Create</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout">Logout</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
<message id="Lms.not-a-directory">Not a directory</message>
|
||||
<message id="Lms.password">Password</message>
|
||||
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
||||
@@ -23,6 +23,7 @@
|
||||
<message id="Lms.quit-confirm">Are you sure?</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.user">User</message>
|
||||
|
||||
<!--Errors-->
|
||||
<message id="Lms.Error.artist-not-found">Artist not found</message>
|
||||
@@ -37,6 +38,7 @@
|
||||
<message id="Lms.Admin.Database.database">Music collection</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
|
||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Music collection</message>
|
||||
<message id="Lms.Admin.Database.never">Never</message>
|
||||
<message id="Lms.Admin.Database.path">Media root directory</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
|
||||
@@ -77,6 +79,7 @@
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.edit">Edit</message>
|
||||
<message id="Lms.Admin.Users.users">Users</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
|
||||
@@ -100,11 +103,15 @@
|
||||
|
||||
<!--Explore-->
|
||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||
<message id="Lms.Explore.all">All</message>
|
||||
<message id="Lms.Explore.artists">Artists</message>
|
||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||
<message id="Lms.Explore.filters">Filters</message>
|
||||
<message id="Lms.Explore.most-played">Most played</message>
|
||||
<message id="Lms.Explore.random">Random</message>
|
||||
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search-placeholder">Search...</message>
|
||||
<message id="Lms.Explore.show-more">Show more</message>
|
||||
<message id="Lms.Explore.tracks">Tracks</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
@@ -112,28 +119,21 @@
|
||||
<message id="Lms.Explore.various-artists">Various artists</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Similar artists</message>
|
||||
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">All artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
|
||||
<message id="Lms.Explore.ArtistsInfo.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.ArtistsInfo.most-played">Top artists</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.ReleaseInfo.similar-releases">Similar albums</message>
|
||||
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disc {1}</message>
|
||||
|
||||
<!--Explore:Releases-->
|
||||
<message id="Lms.Explore.ReleasesInfo.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.ReleasesInfo.most-played">Top albums</message>
|
||||
|
||||
<!--Explore:Tracks-->
|
||||
<message id="Lms.Explore.TracksInfo.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.TracksInfo.most-played">Top tracks</message>
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||
<message id="Lms.Explore.Search.search">Search results</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||
@@ -162,6 +162,7 @@
|
||||
<message id="Lms.Settings.change-password">Change password</message>
|
||||
<message id="Lms.Settings.dark-mode">Dark mode</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
||||
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
||||
@@ -188,7 +189,6 @@
|
||||
<message id="Lms.Settings.transcode-mode.always">Always</message>
|
||||
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Only when format is not supported</message>
|
||||
<message id="Lms.Settings.transcode-mode.never">Never</message>
|
||||
<message id="Lms.Settings.settings">Settings</message>
|
||||
<message id="Lms.Settings.settings-saved">New settings saved!</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+15
-14
@@ -4,13 +4,13 @@
|
||||
|
||||
<!--Common-->
|
||||
<message id="Lms.add">Ajouter</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Appliquer</message>
|
||||
<message id="Lms.discard">Annuler</message>
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.cancel">Annuler</message>
|
||||
<message id="Lms.create">Créer</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout">Logout</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
|
||||
<message id="Lms.not-a-directory">N'est pas un répertoire</message>
|
||||
<message id="Lms.password">Mot de passe</message>
|
||||
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
||||
@@ -23,6 +23,7 @@
|
||||
<message id="Lms.quit-confirm">Êtes-vous sûr ?</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.user">Utilisateur</message>
|
||||
|
||||
<!--Errors-->
|
||||
<message id="Lms.Error.artist-not-found">Cet artiste n'existe pas</message>
|
||||
@@ -37,6 +38,7 @@
|
||||
<message id="Lms.Admin.Database.database">Collection de musiques</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
|
||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Collection de musiques</message>
|
||||
<message id="Lms.Admin.Database.never">Jamais</message>
|
||||
<message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
|
||||
@@ -77,6 +79,7 @@
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.edit">Editer</message>
|
||||
<message id="Lms.Admin.Users.users">Utilisateurs</message>
|
||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
|
||||
@@ -100,11 +103,15 @@
|
||||
|
||||
<!--Explore-->
|
||||
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
||||
<message id="Lms.Explore.all">Tous</message>
|
||||
<message id="Lms.Explore.artists">Artistes</message>
|
||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||
<message id="Lms.Explore.filters">Filtres</message>
|
||||
<message id="Lms.Explore.mostly-played">Plus joués</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-played">Joués récemment</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search-placeholder">Rechercher...</message>
|
||||
<message id="Lms.Explore.show-more">Voir plus</message>
|
||||
<message id="Lms.Explore.tracks">Pistes</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
@@ -112,28 +119,21 @@
|
||||
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Artistes similaires</message>
|
||||
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
|
||||
<message id="Lms.Explore.ArtistsInfo.recently-added">Ajouts récents</message>
|
||||
<message id="Lms.Explore.ArtistsInfo.most-played">Artistes populaires</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.ReleaseInfo.similar-releases">Albums similaires</message>
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disque {1}</message>
|
||||
|
||||
<!--Explore:Releases-->
|
||||
<message id="Lms.Explore.ReleasesInfo.recently-added">Ajouts récents</message>
|
||||
<message id="Lms.Explore.ReleasesInfo.most-played">Albums populaires</message>
|
||||
|
||||
<!--Explore:Tracks-->
|
||||
<message id="Lms.Explore.TracksInfo.recently-added">Ajouts récents</message>
|
||||
<message id="Lms.Explore.TracksInfo.most-played">Pistes populaires</message>
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||
<message id="Lms.Explore.Search.search">Résultats de la recherche</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Effacer</message>
|
||||
@@ -162,6 +162,7 @@
|
||||
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
||||
<message id="Lms.Settings.dark-mode">Mode sombre</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
||||
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.PlayHistory.template">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="page-header">
|
||||
<h2>${tr:Lms.PlayHistory.playhistory}</h2>
|
||||
</div>
|
||||
${entries}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 Lms-horizontal-center">
|
||||
${show-more class="btn-primary Lms-show-more"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayHistory.template.entry">
|
||||
<div class="row Lms-playhistory-entry Lms-entry-hoverable Lms-vertical-align">
|
||||
<div class="col-xs-12 Lms-playhistory-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayHistory.template.entry-artist">
|
||||
${artist class="Lms-artistname text-muted"}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -10,7 +10,7 @@
|
||||
<message id="Lms.PlayQueue.template">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="page-header">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
||||
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
||||
<h4><small>${nb-tracks}</small></h4>
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="col-xs-9 Lms-playqueue-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover"}
|
||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
|
||||
+23
-10
@@ -8,18 +8,31 @@
|
||||
|
||||
<message id="Lms.Explore.Release.template">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="col-md-3">
|
||||
${cover class="Lms-explore-release-cover"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="Lms-header">
|
||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
|
||||
${clusters}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
${container class="Lms-explore-release-entries-container"}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="well well-sm">
|
||||
${<if-has-copyright-or-copyright-url>}
|
||||
<h3>${tr:Lms.Explore.Release.copyright}</h3>
|
||||
${<if-has-copyright>}<small>${copyright}</small>${</if-has-copyright>}
|
||||
${<if-has-copyright-url>}<small>${copyright-url}</small>${</if-has-copyright-url>}
|
||||
${</if-has-copyright-or-copyright-url>}
|
||||
<h3>${tr:Lms.Explore.Release.similar-releases}</h3>
|
||||
${similar-releases}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
|
||||
${clusters}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
${container}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.disc-entry">
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.ReleaseInfo.template">
|
||||
${<if-has-copyright-or-copyright-url>}
|
||||
<h3>${tr:Lms.Explore.Release.copyright}</h3>
|
||||
${<if-has-copyright>}<small>${copyright}</small>${</if-has-copyright>}
|
||||
${<if-has-copyright-url>}<small>${copyright-url}</small>${</if-has-copyright-url>}
|
||||
${</if-has-copyright-or-copyright-url>}
|
||||
<h3>${tr:Lms.Explore.ReleaseInfo.similar-releases}</h3>
|
||||
${similar-releases}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.ReleaseLink.template">
|
||||
<div class="row Lms-explore-releaselink Lms-entry-hoverable">
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<div class="media-heading">
|
||||
${release-name class="Lms-releasename text-primary"}
|
||||
</div>
|
||||
${<if-has-artist>}<small>${artist-name class="Lms-artistname text-muted"}</small>${</if-has-artist>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+22
-8
@@ -3,16 +3,11 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.Releases.template">
|
||||
<div class="page-header">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.releases}</h2>
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
</div>
|
||||
</div>
|
||||
${releases}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 Lms-horizontal-center">
|
||||
@@ -26,7 +21,7 @@
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover"}
|
||||
${cover class="media-object Lms-cover Lms-cover-container-medium"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4 class="media-heading">
|
||||
@@ -40,4 +35,23 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Releases.template.entry-small">
|
||||
<div class="row Lms-explore-releaselink Lms-entry-hoverable">
|
||||
<div class="col-xs-12">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle">
|
||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<div class="media-heading">
|
||||
${release-name class="Lms-releasename text-primary"}
|
||||
</div>
|
||||
${<if-has-artist>}<small>${artist-name class="Lms-artistname text-muted"}</small>${</if-has-artist>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Explore.ReleasesInfo.template">
|
||||
<h3>${tr:Lms.Explore.ReleasesInfo.most-played}</h3>
|
||||
${most-played}
|
||||
<h3>${tr:Lms.Explore.ReleasesInfo.recently-added}</h3>
|
||||
${recently-added}
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Search.template">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.Search.search}</h2>
|
||||
</div>
|
||||
${<if-artists>}
|
||||
<h3>${tr:Lms.Explore.artists}</h3>
|
||||
${artists}
|
||||
${</if-artists>}
|
||||
${<if-releases>}
|
||||
<h3>${tr:Lms.Explore.releases}</h3>
|
||||
${releases}
|
||||
${</if-releases>}
|
||||
${<if-tracks>}
|
||||
<h3>${tr:Lms.Explore.tracks}</h3>
|
||||
${tracks}
|
||||
${</if-tracks>}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Search.template.track-entry">
|
||||
<div class="row Lms-explore-tracks-entry Lms-entry-hoverable Lms-vertical-align">
|
||||
<div class="col-xs-12 Lms-explore-tracks-entry-name">
|
||||
<div class="media">
|
||||
<div class="media-left media-middle hidden-xs">
|
||||
${cover class="media-object Lms-cover"}
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
+48
-1
@@ -2,7 +2,54 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.template">
|
||||
${navbar-top class="Lms-navbar"}
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
${title class="navbar-brand"}
|
||||
<div class="Lms-navbar-search pull-left">
|
||||
${search class="form-control"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>${artists}</li>
|
||||
<li>${releases}</li>
|
||||
<li>${tracks}</li>
|
||||
<li>${playqueue}</li>
|
||||
</ul>
|
||||
<div class="navbar-form navbar-left">
|
||||
${filters class="form-group Lms-clusters-control"}
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
${<if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.administration}"><i class="fa fa-cogs" aria-hidden="true"></i> <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li>${database}</li>
|
||||
<li>${users}</li>
|
||||
</ul>
|
||||
</li>
|
||||
${</if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.user}"><i class="fa fa-user" aria-hidden="true"></i>
|
||||
<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
<li class="divider"></li>
|
||||
<li>${settings}</li>
|
||||
<li>${logout}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
${contents class="Lms-contents"}
|
||||
</div>
|
||||
|
||||
+6
-7
@@ -4,19 +4,18 @@
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.Explore.Tracks.template">
|
||||
<div class="page-header">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.tracks}</h2>
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-search"></i></span>
|
||||
${search}
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
${tracks}
|
||||
</div>
|
||||
</div>
|
||||
${tracks}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 Lms-horizontal-center">
|
||||
<div class="col-lg-8 col-xs-12 Lms-horizontal-center">
|
||||
${show-more class="btn-primary Lms-show-more"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+57
-27
@@ -4,25 +4,15 @@ a, a:link, a:active, a:visited {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 78px;
|
||||
}
|
||||
|
||||
.Lms-vertical-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Lms-horizontal-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Lms-contents {
|
||||
margin-bottom: 78px;
|
||||
}
|
||||
|
||||
.Lms-show-more {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-admin-users-entry {
|
||||
min-height: 48px;
|
||||
padding-top: 8px;
|
||||
@@ -63,6 +53,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
filter: brightness(130%);
|
||||
}
|
||||
|
||||
.Lms-clusters-control {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.Lms-control-btn:hover {
|
||||
color: var(--hover-color);
|
||||
cursor: pointer;
|
||||
@@ -72,18 +66,27 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
filter: brightness(130%);
|
||||
}
|
||||
|
||||
.Lms-cover-container-smaller {
|
||||
}
|
||||
|
||||
.Lms-cover-smaller {
|
||||
width: 64px; /* some cover may not be square */
|
||||
width: 64px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.Lms-cover-container-small {
|
||||
}
|
||||
|
||||
.Lms-cover-small {
|
||||
width: 96px; /* some cover may not be square */
|
||||
width: 96px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.Lms-cover-container-medium {
|
||||
}
|
||||
|
||||
.Lms-cover-medium {
|
||||
width: 128px;
|
||||
border-radius: 3px;
|
||||
@@ -107,6 +110,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.Lms-explore-clusters {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.Lms-explore-contents {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -120,6 +127,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entries-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry-tracknumber {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
@@ -160,12 +171,9 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.Lms-explore-artists-entry {
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.Lms-explore-artists-entry-name {
|
||||
font-style: normal;
|
||||
.Lms-explore-artists-entry-small {
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Lms-explore-artists-input {
|
||||
@@ -177,9 +185,12 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-explore-artistlink {
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
.Lms-explore-artist-entries-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Lms-explore-mode-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.Lms-explore-tracks-entry {
|
||||
@@ -215,8 +226,21 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Lms-navbar {
|
||||
border-radius: 0;
|
||||
.Lms-header {
|
||||
padding-bottom: 9px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.Lms-horizontal-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.Lms-navbar-search {
|
||||
max-width: 175px;
|
||||
padding-top: 7.5px;
|
||||
padding-bottom: 7.5px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-btn {
|
||||
@@ -426,3 +450,9 @@ a.Lms-releasename:hover, a.Lms-releasename:focus {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-show-more {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: collapse.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#collapse
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// COLLAPSE PUBLIC CLASS DEFINITION
|
||||
// ================================
|
||||
|
||||
var Collapse = function (element, options) {
|
||||
this.$element = $(element)
|
||||
this.options = $.extend({}, Collapse.DEFAULTS, options)
|
||||
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
|
||||
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
|
||||
this.transitioning = null
|
||||
|
||||
if (this.options.parent) {
|
||||
this.$parent = this.getParent()
|
||||
} else {
|
||||
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
|
||||
}
|
||||
|
||||
if (this.options.toggle) this.toggle()
|
||||
}
|
||||
|
||||
Collapse.VERSION = '3.3.6'
|
||||
|
||||
Collapse.TRANSITION_DURATION = 350
|
||||
|
||||
Collapse.DEFAULTS = {
|
||||
toggle: true
|
||||
}
|
||||
|
||||
Collapse.prototype.dimension = function () {
|
||||
var hasWidth = this.$element.hasClass('width')
|
||||
return hasWidth ? 'width' : 'height'
|
||||
}
|
||||
|
||||
Collapse.prototype.show = function () {
|
||||
if (this.transitioning || this.$element.hasClass('in')) return
|
||||
|
||||
var activesData
|
||||
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
|
||||
|
||||
if (actives && actives.length) {
|
||||
activesData = actives.data('bs.collapse')
|
||||
if (activesData && activesData.transitioning) return
|
||||
}
|
||||
|
||||
var startEvent = $.Event('show.bs.collapse')
|
||||
this.$element.trigger(startEvent)
|
||||
if (startEvent.isDefaultPrevented()) return
|
||||
|
||||
if (actives && actives.length) {
|
||||
Plugin.call(actives, 'hide')
|
||||
activesData || actives.data('bs.collapse', null)
|
||||
}
|
||||
|
||||
var dimension = this.dimension()
|
||||
|
||||
this.$element
|
||||
.removeClass('collapse')
|
||||
.addClass('collapsing')[dimension](0)
|
||||
.attr('aria-expanded', true)
|
||||
|
||||
this.$trigger
|
||||
.removeClass('collapsed')
|
||||
.attr('aria-expanded', true)
|
||||
|
||||
this.transitioning = 1
|
||||
|
||||
var complete = function () {
|
||||
this.$element
|
||||
.removeClass('collapsing')
|
||||
.addClass('collapse in')[dimension]('')
|
||||
this.transitioning = 0
|
||||
this.$element
|
||||
.trigger('shown.bs.collapse')
|
||||
}
|
||||
|
||||
if (!$.support.transition) return complete.call(this)
|
||||
|
||||
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
|
||||
|
||||
this.$element
|
||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
|
||||
}
|
||||
|
||||
Collapse.prototype.hide = function () {
|
||||
if (this.transitioning || !this.$element.hasClass('in')) return
|
||||
|
||||
var startEvent = $.Event('hide.bs.collapse')
|
||||
this.$element.trigger(startEvent)
|
||||
if (startEvent.isDefaultPrevented()) return
|
||||
|
||||
var dimension = this.dimension()
|
||||
|
||||
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
|
||||
|
||||
this.$element
|
||||
.addClass('collapsing')
|
||||
.removeClass('collapse in')
|
||||
.attr('aria-expanded', false)
|
||||
|
||||
this.$trigger
|
||||
.addClass('collapsed')
|
||||
.attr('aria-expanded', false)
|
||||
|
||||
this.transitioning = 1
|
||||
|
||||
var complete = function () {
|
||||
this.transitioning = 0
|
||||
this.$element
|
||||
.removeClass('collapsing')
|
||||
.addClass('collapse')
|
||||
.trigger('hidden.bs.collapse')
|
||||
}
|
||||
|
||||
if (!$.support.transition) return complete.call(this)
|
||||
|
||||
this.$element
|
||||
[dimension](0)
|
||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
|
||||
}
|
||||
|
||||
Collapse.prototype.toggle = function () {
|
||||
this[this.$element.hasClass('in') ? 'hide' : 'show']()
|
||||
}
|
||||
|
||||
Collapse.prototype.getParent = function () {
|
||||
return $(this.options.parent)
|
||||
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
|
||||
.each($.proxy(function (i, element) {
|
||||
var $element = $(element)
|
||||
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
|
||||
}, this))
|
||||
.end()
|
||||
}
|
||||
|
||||
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
|
||||
var isOpen = $element.hasClass('in')
|
||||
|
||||
$element.attr('aria-expanded', isOpen)
|
||||
$trigger
|
||||
.toggleClass('collapsed', !isOpen)
|
||||
.attr('aria-expanded', isOpen)
|
||||
}
|
||||
|
||||
function getTargetFromTrigger($trigger) {
|
||||
var href
|
||||
var target = $trigger.attr('data-target')
|
||||
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
|
||||
|
||||
return $(target)
|
||||
}
|
||||
|
||||
|
||||
// COLLAPSE PLUGIN DEFINITION
|
||||
// ==========================
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.collapse')
|
||||
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
|
||||
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
|
||||
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.collapse
|
||||
|
||||
$.fn.collapse = Plugin
|
||||
$.fn.collapse.Constructor = Collapse
|
||||
|
||||
|
||||
// COLLAPSE NO CONFLICT
|
||||
// ====================
|
||||
|
||||
$.fn.collapse.noConflict = function () {
|
||||
$.fn.collapse = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// COLLAPSE DATA-API
|
||||
// =================
|
||||
|
||||
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
|
||||
var $this = $(this)
|
||||
|
||||
if (!$this.attr('data-target')) e.preventDefault()
|
||||
|
||||
var $target = getTargetFromTrigger($this)
|
||||
var data = $target.data('bs.collapse')
|
||||
var option = data ? 'toggle' : $this.data()
|
||||
|
||||
Plugin.call($target, option)
|
||||
})
|
||||
|
||||
}(jQuery);
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: dropdown.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#dropdowns
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// DROPDOWN CLASS DEFINITION
|
||||
// =========================
|
||||
|
||||
var backdrop = '.dropdown-backdrop'
|
||||
var toggle = '[data-toggle="dropdown"]'
|
||||
var Dropdown = function (element) {
|
||||
$(element).on('click.bs.dropdown', this.toggle)
|
||||
}
|
||||
|
||||
Dropdown.VERSION = '3.3.6'
|
||||
|
||||
function getParent($this) {
|
||||
var selector = $this.attr('data-target')
|
||||
|
||||
if (!selector) {
|
||||
selector = $this.attr('href')
|
||||
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
|
||||
}
|
||||
|
||||
var $parent = selector && $(selector)
|
||||
|
||||
return $parent && $parent.length ? $parent : $this.parent()
|
||||
}
|
||||
|
||||
function clearMenus(e) {
|
||||
if (e && e.which === 3) return
|
||||
$(backdrop).remove()
|
||||
$(toggle).each(function () {
|
||||
var $this = $(this)
|
||||
var $parent = getParent($this)
|
||||
var relatedTarget = { relatedTarget: this }
|
||||
|
||||
if (!$parent.hasClass('open')) return
|
||||
|
||||
if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
|
||||
|
||||
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
$this.attr('aria-expanded', 'false')
|
||||
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
|
||||
})
|
||||
}
|
||||
|
||||
Dropdown.prototype.toggle = function (e) {
|
||||
var $this = $(this)
|
||||
|
||||
if ($this.is('.disabled, :disabled')) return
|
||||
|
||||
var $parent = getParent($this)
|
||||
var isActive = $parent.hasClass('open')
|
||||
|
||||
clearMenus()
|
||||
|
||||
if (!isActive) {
|
||||
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
|
||||
// if mobile we use a backdrop because click events don't delegate
|
||||
$(document.createElement('div'))
|
||||
.addClass('dropdown-backdrop')
|
||||
.insertAfter($(this))
|
||||
.on('click', clearMenus)
|
||||
}
|
||||
|
||||
var relatedTarget = { relatedTarget: this }
|
||||
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
$this
|
||||
.trigger('focus')
|
||||
.attr('aria-expanded', 'true')
|
||||
|
||||
$parent
|
||||
.toggleClass('open')
|
||||
.trigger($.Event('shown.bs.dropdown', relatedTarget))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Dropdown.prototype.keydown = function (e) {
|
||||
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
|
||||
|
||||
var $this = $(this)
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if ($this.is('.disabled, :disabled')) return
|
||||
|
||||
var $parent = getParent($this)
|
||||
var isActive = $parent.hasClass('open')
|
||||
|
||||
if (!isActive && e.which != 27 || isActive && e.which == 27) {
|
||||
if (e.which == 27) $parent.find(toggle).trigger('focus')
|
||||
return $this.trigger('click')
|
||||
}
|
||||
|
||||
var desc = ' li:not(.disabled):visible a'
|
||||
var $items = $parent.find('.dropdown-menu' + desc)
|
||||
|
||||
if (!$items.length) return
|
||||
|
||||
var index = $items.index(e.target)
|
||||
|
||||
if (e.which == 38 && index > 0) index-- // up
|
||||
if (e.which == 40 && index < $items.length - 1) index++ // down
|
||||
if (!~index) index = 0
|
||||
|
||||
$items.eq(index).trigger('focus')
|
||||
}
|
||||
|
||||
|
||||
// DROPDOWN PLUGIN DEFINITION
|
||||
// ==========================
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.dropdown')
|
||||
|
||||
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
|
||||
if (typeof option == 'string') data[option].call($this)
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.dropdown
|
||||
|
||||
$.fn.dropdown = Plugin
|
||||
$.fn.dropdown.Constructor = Dropdown
|
||||
|
||||
|
||||
// DROPDOWN NO CONFLICT
|
||||
// ====================
|
||||
|
||||
$.fn.dropdown.noConflict = function () {
|
||||
$.fn.dropdown = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// APPLY TO STANDARD DROPDOWN ELEMENTS
|
||||
// ===================================
|
||||
|
||||
$(document)
|
||||
.on('click.bs.dropdown.data-api', clearMenus)
|
||||
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
|
||||
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
|
||||
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
|
||||
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
|
||||
|
||||
}(jQuery);
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
/* ========================================================================
|
||||
* Bootstrap: transition.js v3.3.6
|
||||
* http://getbootstrap.com/javascript/#transitions
|
||||
* ========================================================================
|
||||
* Copyright 2011-2015 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
|
||||
// ============================================================
|
||||
|
||||
function transitionEnd() {
|
||||
var el = document.createElement('bootstrap')
|
||||
|
||||
var transEndEventNames = {
|
||||
WebkitTransition : 'webkitTransitionEnd',
|
||||
MozTransition : 'transitionend',
|
||||
OTransition : 'oTransitionEnd otransitionend',
|
||||
transition : 'transitionend'
|
||||
}
|
||||
|
||||
for (var name in transEndEventNames) {
|
||||
if (el.style[name] !== undefined) {
|
||||
return { end: transEndEventNames[name] }
|
||||
}
|
||||
}
|
||||
|
||||
return false // explicit for ie8 ( ._.)
|
||||
}
|
||||
|
||||
// http://blog.alexmaccaw.com/css-transitions
|
||||
$.fn.emulateTransitionEnd = function (duration) {
|
||||
var called = false
|
||||
var $el = this
|
||||
$(this).one('bsTransitionEnd', function () { called = true })
|
||||
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
|
||||
setTimeout(callback, duration)
|
||||
return this
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$.support.transition = transitionEnd()
|
||||
|
||||
if (!$.support.transition) return
|
||||
|
||||
$.event.special.bsTransitionEnd = {
|
||||
bindType: $.support.transition.end,
|
||||
delegateType: $.support.transition.end,
|
||||
handle: function (e) {
|
||||
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}(jQuery);
|
||||
@@ -44,9 +44,9 @@ namespace CoverArt
|
||||
Release,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Type type;
|
||||
Database::IdType id;
|
||||
std::size_t size;
|
||||
std::size_t size;
|
||||
|
||||
bool operator==(const CacheEntryDesc& other) const
|
||||
{
|
||||
@@ -114,7 +114,7 @@ namespace CoverArt
|
||||
void saveToCache(const CacheEntryDesc& entryDesc, const Image& image);
|
||||
std::optional<Image> loadFromCache(const CacheEntryDesc& entryDesc);
|
||||
|
||||
static inline constexpr std::size_t _maxCacheEntries {300};
|
||||
static inline constexpr std::size_t _maxCacheEntries {1000};
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
static inline constexpr std::size_t _maxFileSize {10000000};
|
||||
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
|
||||
@@ -74,91 +74,133 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Session& session,
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
Artist::SortMethod sortMethod)
|
||||
std::optional<TrackArtistLink::Type> linkType)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
WhereClause where;
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT a FROM artist a";
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
if (!keywords.empty())
|
||||
{
|
||||
WhereClause whereKeywordsName;
|
||||
WhereClause whereKeywordsSortName;
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
|
||||
for (auto keyword : keywords)
|
||||
for (const std::string& keyword : keywords)
|
||||
{
|
||||
whereKeywordsName.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
whereKeywordsSortName.And(WhereClause("a.sort_name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
clauses.push_back("a.name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
|
||||
sortClauses.push_back("a.sort_name LIKE ?");
|
||||
query.bind("%%" + keyword + "%%");
|
||||
}
|
||||
|
||||
where.And(whereKeywordsName.Or(whereKeywordsSortName));
|
||||
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
}
|
||||
|
||||
if (!clusterIds.empty() || linkType)
|
||||
{
|
||||
oss << " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id";
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
if (linkType)
|
||||
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
oss << " ORDER BY a.name COLLATE NOCASE";
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
oss << " ORDER BY a.sort_name COLLATE NOCASE";
|
||||
break;
|
||||
}
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() );
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
Artist::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = getQuery(session, {}, {}, std::nullopt, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Artist>();
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().find<Artist>()};
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query;
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIds(Session& session)
|
||||
{
|
||||
@@ -168,6 +210,21 @@ Artist::getAllIds(Session& session)
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAllOrphans(Session& session)
|
||||
{
|
||||
@@ -197,8 +254,8 @@ Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMe
|
||||
assert(!clusters.empty());
|
||||
|
||||
session.checkSharedLocked();
|
||||
bool more;
|
||||
return getByFilter(session, clusters, {}, {}, sortMethod, {}, {}, more);
|
||||
bool more{};
|
||||
return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more);
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
@@ -207,18 +264,31 @@ Artist::getByFilter(Session& session,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords, linkType, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -230,14 +300,29 @@ Artist::getByFilter(Session& session,
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> limit)
|
||||
Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().query<Artist::pointer>("SELECT a from artist a INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id INNER JOIN track t ON t.id = t_a_l.track_id")
|
||||
.where("t.file_last_write > ?").bind(after)
|
||||
.groupBy("a.id")
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(*after);
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -427,7 +512,6 @@ void
|
||||
Artist::setSortName(const std::string& sortName)
|
||||
{
|
||||
_sortName = std::string(sortName, 0 , _maxNameLength);
|
||||
LMS_LOG(DB, DEBUG) << "SORT NAME = '" << _sortName << "'";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -46,9 +46,25 @@ Db::Db(const std::filesystem::path& dbPath)
|
||||
void
|
||||
Db::executeSql(const std::string& sql)
|
||||
{
|
||||
auto connection {_connectionPool->getConnection()};
|
||||
ScopedConnection connection {*_connectionPool};
|
||||
connection->executeSql(sql);
|
||||
_connectionPool->returnConnection(std::move(connection));
|
||||
}
|
||||
|
||||
|
||||
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
|
||||
: _connectionPool {pool}
|
||||
, _connection {_connectionPool.getConnection()}
|
||||
{
|
||||
}
|
||||
|
||||
Db::ScopedConnection::~ScopedConnection()
|
||||
{
|
||||
_connectionPool.returnConnection(std::move(_connection));
|
||||
}
|
||||
|
||||
Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const
|
||||
{
|
||||
return _connection.get();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -31,6 +31,45 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
|
||||
for (const std::string& keyword : keywords)
|
||||
query.where("r.name LIKE ?").bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const IdType clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name {std::string(name, 0 , _maxNameLength)},
|
||||
_MBID {MBID ? MBID->getAsString() : ""}
|
||||
@@ -84,13 +123,13 @@ Release::getCount(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAll(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
Release::getAll(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.orderBy("name COLLATE NOCASE");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
@@ -123,17 +162,34 @@ Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offs
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllRandom(Session& session, std::optional<std::size_t> size)
|
||||
Release::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Release>()
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.orderBy("RANDOM()");
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT DISTINCT r from release r", clusterIds,{})};
|
||||
|
||||
Wt::Dbo::collection<pointer> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Release::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT DISTINCT r.id from release r", clusterIds,{})};
|
||||
|
||||
Wt::Dbo::collection<IdType> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllOrphans(Session& session)
|
||||
{
|
||||
@@ -145,18 +201,34 @@ Release::getAllOrphans(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset, std::optional<std::size_t> limit)
|
||||
Release::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session.getDboSession().query<Release::pointer>("SELECT r from release r INNER JOIN track t ON r.id = t.release_id")
|
||||
.where("t.file_last_write > ?").bind(after)
|
||||
.groupBy("r.id")
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("r.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
@@ -173,47 +245,6 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Release::pointer>
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT r FROM release r";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN track t ON t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY r.name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Release::pointer> query = session.getDboSession().query<Release::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
{
|
||||
@@ -222,26 +253,27 @@ Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
return getByFilter(session, clusters, {}, {}, {}, moreResults);
|
||||
return getByFilter(session, clusters, {}, std::nullopt, moreResults);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, keywords)
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (size && res.size() == static_cast<std::size_t>(*size) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
|
||||
@@ -378,6 +378,7 @@ Session::prepareTables()
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
|
||||
|
||||
@@ -32,9 +32,46 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
|
||||
for (const std::string& keyword : keywords)
|
||||
query.where("t.name LIKE ?").bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
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 IdType clusterId : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Track::Track(const std::filesystem::path& p)
|
||||
:
|
||||
_filePath( p.string() )
|
||||
: _filePath {p.string()}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -58,17 +95,34 @@ Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllRandom(Session& session, std::optional<std::size_t> limit)
|
||||
Track::getAllRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res {session.getDboSession().find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.orderBy("RANDOM()")};
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
|
||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<pointer>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Track::getAllIdsRandom(Session& session, const std::set<IdType>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<IdType>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<IdType> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<IdType>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<IdType>
|
||||
Track::getAllIds(Session& session)
|
||||
{
|
||||
@@ -147,16 +201,30 @@ Track::getMBIDDuplicates(Session& session)
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> limit)
|
||||
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res = session.getDboSession().find<Track>()
|
||||
.where("file_last_write > ?").bind(after)
|
||||
.orderBy("file_last_write DESC")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("t.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
@@ -225,66 +293,21 @@ Track::hasTrackFeatures() const
|
||||
return (_trackFeatures.lock() != Database::TrackFeatures::pointer());
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query< Track::pointer >
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT t FROM track t";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("t.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY t.name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Track::pointer> query = session.getDboSession().query<Track::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
Wt::Dbo::collection<pointer> collection = createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, keywords)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
|
||||
if (size && (res.size() == static_cast<std::size_t>(*size) + 1))
|
||||
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -340,12 +363,10 @@ Track::getByClusters(Session& session,
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
|
||||
return getByFilter(session,
|
||||
clusters,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}, // keywords
|
||||
std::nullopt, // range
|
||||
moreResults);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -106,6 +107,23 @@ TrackList::getById(Session& session, IdType id)
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackList::getCount() const
|
||||
{
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackList::getEntry(std::size_t pos) const
|
||||
{
|
||||
Wt::Dbo::ptr<TrackListEntry> res;
|
||||
|
||||
auto entries = getEntries(pos, 1);
|
||||
if (!entries.empty())
|
||||
res = entries.front();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>>
|
||||
TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||
@@ -139,22 +157,180 @@ TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<st
|
||||
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<TrackListEntry>
|
||||
TrackList::getEntry(std::size_t pos) const
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType)
|
||||
{
|
||||
Wt::Dbo::ptr<TrackListEntry> res;
|
||||
auto query {session.query<Artist::pointer>(queryStr)};
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
||||
|
||||
auto entries = getEntries(pos, 1);
|
||||
if (!entries.empty())
|
||||
res = entries.front();
|
||||
query.where("p.id = ?").bind(tracklistId);
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Release::pointer>
|
||||
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Release::pointer>(queryStr)};
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
||||
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
||||
|
||||
query.where("p.id = ?").bind(tracklistId);
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Track::pointer>
|
||||
createTracksQuery(Wt::Dbo::Session& session, IdType tracklistId, const std::set<IdType>& clusterIds)
|
||||
{
|
||||
auto query {session.query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")};
|
||||
|
||||
query.where("p.id = ?").bind(tracklistId);
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
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 (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT DISTINCT a from artist a", self()->id(), clusterIds, linkType)
|
||||
.orderBy("p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackList::getCount() const
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
return _entries.size();
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT DISTINCT r from release r", self()->id(), clusterIds)
|
||||
.orderBy("p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = createTracksQuery(*session(), self()->id(), clusterIds)
|
||||
.orderBy("p_e.id DESC")
|
||||
.groupBy("t.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Cluster>>
|
||||
@@ -235,48 +411,85 @@ TrackList::getDuration() const
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getTopArtists(std::size_t limit) const
|
||||
TrackList::getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = session()->query<Artist::pointer>("SELECT a from artist a INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
||||
.where("p.id = ?").bind(self()->id())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)};
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getTopReleases(std::size_t limit) const
|
||||
TrackList::getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> res = session()->query<Release::pointer>("SELECT r from release r INNER JOIN track t ON t.release_id = r.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
||||
.where("p.id = ?").bind(self()->id())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)};
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Release::pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
TrackList::getTopTracks(std::size_t limit) const
|
||||
TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> res = session()->query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
|
||||
.where("p.id = ?").bind(self()->id())
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.limit(static_cast<int>(limit));
|
||||
auto query {createTracksQuery(*session(), self()->id(), clusterIds)};
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<Track::pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
|
||||
|
||||
@@ -70,14 +70,21 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session, Wt::WDateTime after, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
// Accessors
|
||||
|
||||
@@ -39,6 +39,24 @@ class Db
|
||||
std::shared_mutex& getMutex() { return _sharedMutex; }
|
||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||
|
||||
class ScopedConnection
|
||||
{
|
||||
public:
|
||||
ScopedConnection(Wt::Dbo::SqlConnectionPool& pool);
|
||||
~ScopedConnection();
|
||||
|
||||
ScopedConnection(const ScopedConnection& ) = delete;
|
||||
ScopedConnection(ScopedConnection&& ) = delete;
|
||||
ScopedConnection& operator=(const ScopedConnection& ) = delete;
|
||||
ScopedConnection& operator=(ScopedConnection&& ) = delete;
|
||||
|
||||
Wt::Dbo::SqlConnection* operator->() const;
|
||||
|
||||
private:
|
||||
Wt::Dbo::SqlConnectionPool& _connectionPool;
|
||||
std::unique_ptr<Wt::Dbo::SqlConnection> _connection;
|
||||
};
|
||||
|
||||
class ScopedNoForeignKeys
|
||||
{
|
||||
public:
|
||||
@@ -51,9 +69,13 @@ class Db
|
||||
_db.executeSql("PRAGMA foreign_keys=ON");
|
||||
}
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete;
|
||||
ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete;
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
};
|
||||
|
||||
void executeSql(const std::string& sql);
|
||||
|
||||
@@ -52,19 +52,19 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // no track related
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<pointer> getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
|
||||
@@ -68,16 +68,16 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<pointer> getAllRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> limit = std::nullopt);
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
||||
static std::vector<pointer> getLastWritten(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Database {
|
||||
@@ -51,9 +53,9 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
|
||||
|
||||
// Stats utility
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(std::size_t limit = 1) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
// Search utility
|
||||
static pointer get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user);
|
||||
@@ -82,6 +84,10 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
|
||||
|
||||
std::vector<IdType> getTrackIds() const;
|
||||
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
|
||||
@@ -21,12 +21,19 @@
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace Database {
|
||||
namespace Database
|
||||
{
|
||||
using IdType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
static inline bool IdIsValid(IdType id)
|
||||
{
|
||||
return id != Wt::Dbo::dbo_default_traits::invalidId();
|
||||
}
|
||||
|
||||
struct Range
|
||||
{
|
||||
std::size_t offset {};
|
||||
std::size_t limit {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -677,7 +677,7 @@ handleGetRandomSongsRequest(RequestContext& context)
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
auto tracks {Track::getAllRandom(context.dbSession, size)};
|
||||
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
|
||||
@@ -710,16 +710,17 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
if (type == "random")
|
||||
{
|
||||
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||
releases = Release::getAllRandom(context.dbSession, size);
|
||||
releases = Release::getAllRandom(context.dbSession, {}, size);
|
||||
}
|
||||
else if (type == "newest")
|
||||
{
|
||||
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
||||
releases = Release::getLastWritten(context.dbSession, after, offset, size);
|
||||
bool moreResults {};
|
||||
releases = Release::getLastWritten(context.dbSession, after, {}, Range {offset, size}, moreResults);
|
||||
}
|
||||
else if (type == "alphabeticalByName")
|
||||
{
|
||||
releases = Release::getAll(context.dbSession, offset, size);
|
||||
releases = Release::getAll(context.dbSession, Range {offset, size});
|
||||
}
|
||||
else if (type == "alphabeticalByArtist")
|
||||
{
|
||||
@@ -748,7 +749,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
if (cluster)
|
||||
{
|
||||
bool more;
|
||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more);
|
||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -935,7 +936,7 @@ handleGetArtistsRequest(RequestContext& context)
|
||||
{},
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
{}, {}, more)};
|
||||
std::nullopt, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
||||
|
||||
@@ -966,7 +967,8 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
{
|
||||
directoryNode.setAttribute("name", "Music");
|
||||
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
||||
bool moreResults{};
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1078,7 +1080,7 @@ handleGetIndexesRequest(RequestContext& context)
|
||||
{},
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
{}, {}, more)};
|
||||
std::nullopt, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1296,7 +1298,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
||||
|
||||
bool more;
|
||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)};
|
||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, Range {offset, size}, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
@@ -1367,19 +1369,19 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
bool more;
|
||||
{
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, artistOffset, artistCount, more)};
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, Range {artistOffset, artistCount}, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)};
|
||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, Range {albumOffset, albumCount}, more)};
|
||||
for (const Release::pointer& release : releases)
|
||||
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)};
|
||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, Range {songOffset, songCount}, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
@@ -24,10 +24,15 @@ namespace Random {
|
||||
RandGenerator& getRandGenerator()
|
||||
{
|
||||
static thread_local std::random_device rd;
|
||||
static thread_local std::mt19937 randGenerator(rd());
|
||||
static thread_local RandGenerator randGenerator(rd());
|
||||
|
||||
return randGenerator;
|
||||
}
|
||||
|
||||
RandGenerator createSeededGenerator(std::size_t seed)
|
||||
{
|
||||
return RandGenerator {seed};
|
||||
}
|
||||
|
||||
} // Random
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Random {
|
||||
using RandGenerator = std::mt19937;
|
||||
RandGenerator& getRandGenerator();
|
||||
|
||||
RandGenerator createSeededGenerator(std::size_t seed);
|
||||
|
||||
template <typename T>
|
||||
T
|
||||
getRandom(T min, T max)
|
||||
|
||||
@@ -7,7 +7,6 @@ add_executable(lms
|
||||
ui/LmsTheme.cpp
|
||||
ui/MediaPlayer.cpp
|
||||
ui/PlayQueueView.cpp
|
||||
ui/PlayHistoryView.cpp
|
||||
ui/SettingsView.cpp
|
||||
ui/TrackStringUtils.cpp
|
||||
ui/admin/DatabaseSettingsView.cpp
|
||||
@@ -16,19 +15,15 @@ add_executable(lms
|
||||
ui/admin/UserView.cpp
|
||||
ui/admin/UsersView.cpp
|
||||
ui/common/Validators.cpp
|
||||
ui/explore/ArtistInfoView.cpp
|
||||
ui/explore/ArtistLink.cpp
|
||||
ui/explore/ArtistsInfoView.cpp
|
||||
ui/explore/ArtistListHelpers.cpp
|
||||
ui/explore/ArtistView.cpp
|
||||
ui/explore/ArtistsView.cpp
|
||||
ui/explore/Explore.cpp
|
||||
ui/explore/Filters.cpp
|
||||
ui/explore/ReleaseInfoView.cpp
|
||||
ui/explore/ReleaseLink.cpp
|
||||
ui/explore/ReleasesInfoView.cpp
|
||||
ui/explore/ReleaseListHelpers.cpp
|
||||
ui/explore/ReleasesView.cpp
|
||||
ui/explore/ReleaseView.cpp
|
||||
ui/explore/TracksInfoView.cpp
|
||||
ui/explore/SearchView.cpp
|
||||
ui/explore/TracksView.cpp
|
||||
ui/resource/AudioFileResource.cpp
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
|
||||
+14
-1
@@ -77,7 +77,20 @@ std::vector<std::string> generateWtConfig(std::string execPath)
|
||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||
pt.put("server.application-settings.log-config", ServiceProvider<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
||||
pt.put("server.application-settings.behind-reverse-proxy", ServiceProvider<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||
pt.put("server.application-settings.progressive-bootstrap", true);
|
||||
|
||||
{
|
||||
boost::property_tree::ptree viewport;
|
||||
viewport.put("<xmlattr>.name", "viewport");
|
||||
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
||||
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
||||
}
|
||||
{
|
||||
boost::property_tree::ptree themeColor;
|
||||
themeColor.put("<xmlattr>.name", "theme-color");
|
||||
themeColor.put("<xmlattr>.content", "#303030");
|
||||
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
||||
}
|
||||
|
||||
|
||||
std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out);
|
||||
boost::property_tree::xml_parser::write_xml(oss, pt);
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WEnvironment.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WNavigationBar.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WText.h>
|
||||
@@ -35,6 +33,8 @@
|
||||
#include "database/Release.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "explore/Explore.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/SearchView.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -50,13 +50,14 @@
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "LmsTheme.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "PlayHistoryView.hpp"
|
||||
#include "PlayQueueView.hpp"
|
||||
#include "SettingsView.hpp"
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
static constexpr const char* defaultPath {"/releases"};
|
||||
|
||||
std::unique_ptr<Wt::WApplication>
|
||||
LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups)
|
||||
{
|
||||
@@ -115,7 +116,6 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
_dbSession {db},
|
||||
_appGroups {appGroups}
|
||||
{
|
||||
addMetaHeader(Wt::MetaHeaderType::Meta, "viewport", "width=device-width, user-scalable=no");
|
||||
|
||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||
|
||||
@@ -125,31 +125,27 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
messageResourceBundle().use(appRoot() + "admin-user");
|
||||
messageResourceBundle().use(appRoot() + "admin-users");
|
||||
messageResourceBundle().use(appRoot() + "artist");
|
||||
messageResourceBundle().use(appRoot() + "artistinfo");
|
||||
messageResourceBundle().use(appRoot() + "artistlink");
|
||||
messageResourceBundle().use(appRoot() + "artists");
|
||||
messageResourceBundle().use(appRoot() + "artistsinfo");
|
||||
messageResourceBundle().use(appRoot() + "error");
|
||||
messageResourceBundle().use(appRoot() + "explore");
|
||||
messageResourceBundle().use(appRoot() + "login");
|
||||
messageResourceBundle().use(appRoot() + "mediaplayer");
|
||||
messageResourceBundle().use(appRoot() + "messages");
|
||||
messageResourceBundle().use(appRoot() + "playqueue");
|
||||
messageResourceBundle().use(appRoot() + "playhistory");
|
||||
messageResourceBundle().use(appRoot() + "release");
|
||||
messageResourceBundle().use(appRoot() + "releaseinfo");
|
||||
messageResourceBundle().use(appRoot() + "releaselink");
|
||||
messageResourceBundle().use(appRoot() + "releases");
|
||||
messageResourceBundle().use(appRoot() + "releasesinfo");
|
||||
messageResourceBundle().use(appRoot() + "search");
|
||||
messageResourceBundle().use(appRoot() + "settings");
|
||||
messageResourceBundle().use(appRoot() + "templates");
|
||||
messageResourceBundle().use(appRoot() + "tracks");
|
||||
messageResourceBundle().use(appRoot() + "tracksinfo");
|
||||
|
||||
// Require js here to avoid async problems
|
||||
requireJQuery("js/jquery-1.10.2.min.js");
|
||||
require("js/mediaplayer.js");
|
||||
require("js/bootstrap-notify.js");
|
||||
require("js/collapse.js");
|
||||
require("js/dropdown.js");
|
||||
require("js/transition.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
@@ -245,7 +241,7 @@ LmsApplication::finalize()
|
||||
Wt::WLink
|
||||
LmsApplication::createArtistLink(Database::Artist::pointer artist)
|
||||
{
|
||||
return Wt::WLink(Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id()));
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id())};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
@@ -265,7 +261,7 @@ LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addTex
|
||||
Wt::WLink
|
||||
LmsApplication::createReleaseLink(Database::Release::pointer release)
|
||||
{
|
||||
return Wt::WLink(Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id()));
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id())};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
@@ -289,10 +285,10 @@ LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete
|
||||
{
|
||||
switch (cluster->getType().id() % 6)
|
||||
{
|
||||
case 0: return "label-primary";
|
||||
case 1: return "label-default";
|
||||
case 2: return "label-info";
|
||||
case 3: return "label-warning";
|
||||
case 0: return "label-info";
|
||||
case 1: return "label-warning";
|
||||
case 2: return "label-primary";
|
||||
case 3: return "label-default";
|
||||
case 4: return "label-success";
|
||||
case 5: return "label-danger";
|
||||
}
|
||||
@@ -316,7 +312,7 @@ LmsApplication::handleException(LmsApplicationException& e)
|
||||
Wt::WTemplate* t {root()->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Error.template"))};
|
||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
t->bindString("error", e.what());
|
||||
t->bindString("error", e.what(), Wt::TextFormat::Plain);
|
||||
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
|
||||
btn->clicked().connect([this]()
|
||||
{
|
||||
@@ -337,7 +333,7 @@ enum IdxRoot
|
||||
{
|
||||
IdxExplore = 0,
|
||||
IdxPlayQueue,
|
||||
IdxPlayHistory,
|
||||
IdxSearch,
|
||||
IdxSettings,
|
||||
IdxAdminDatabase,
|
||||
IdxAdminUsers,
|
||||
@@ -360,7 +356,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
{ "/release", IdxExplore, false },
|
||||
{ "/tracks", IdxExplore, false },
|
||||
{ "/playqueue", IdxPlayQueue, false },
|
||||
{ "/playhistory", IdxPlayHistory, false },
|
||||
{ "/search", IdxSearch, false },
|
||||
{ "/settings", IdxSettings, false },
|
||||
{ "/admin/database", IdxAdminDatabase, true },
|
||||
{ "/admin/users", IdxAdminUsers, true },
|
||||
@@ -381,7 +377,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
wApp->setInternalPath("/artists", true);
|
||||
wApp->setInternalPath(defaultPath, true);
|
||||
}
|
||||
|
||||
LmsApplicationGroup&
|
||||
@@ -436,69 +432,35 @@ LmsApplication::createHome()
|
||||
|
||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")))};
|
||||
|
||||
// Navbar
|
||||
Wt::WNavigationBar* navbar {main->bindNew<Wt::WNavigationBar>("navbar-top")};
|
||||
navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"});
|
||||
navbar->setResponsive(true);
|
||||
main->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
// MediaPlayer
|
||||
_mediaPlayer = main->bindNew<MediaPlayer>("player");
|
||||
|
||||
Wt::WMenu* menu {navbar->addMenu(std::make_unique<Wt::WMenu>())};
|
||||
main->bindNew<Wt::WAnchor>("title", Wt::WLink {Wt::LinkType::InternalPath, defaultPath}, "LMS");
|
||||
main->bindNew<Wt::WAnchor>("artists", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}, Wt::WString::tr("Lms.Explore.artists"));
|
||||
main->bindNew<Wt::WAnchor>("releases", Wt::WLink {Wt::LinkType::InternalPath, "/releases"}, Wt::WString::tr("Lms.Explore.releases"));
|
||||
main->bindNew<Wt::WAnchor>("tracks", Wt::WLink {Wt::LinkType::InternalPath, "/tracks"}, Wt::WString::tr("Lms.Explore.tracks"));
|
||||
|
||||
Filters* filters {main->bindNew<Filters>("filters")};
|
||||
main->bindNew<Wt::WAnchor>("playqueue", Wt::WLink {Wt::LinkType::InternalPath, "/playqueue"}, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||
main->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
|
||||
main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
|
||||
|
||||
{
|
||||
auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(1, Wt::WString::tr("Lms.Explore.releases"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/releases"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(2, Wt::WString::tr("Lms.Explore.tracks"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/tracks"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(3, Wt::WString::tr("Lms.PlayQueue.playqueue"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playqueue"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = menu->insertItem(4, Wt::WString::tr("Lms.PlayHistory.playhistory"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playhistory"));
|
||||
menuItem->setSelectable(false);
|
||||
auto* logout {main->bindNew<Wt::WAnchor>("logout")};
|
||||
logout->setText(Wt::WString::tr("Lms.logout"));
|
||||
logout->clicked().connect(this, &LmsApplication::handleUserLoggedOut);
|
||||
}
|
||||
|
||||
Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique<Wt::WMenu>(), Wt::AlignmentFlag::Right);
|
||||
std::size_t itemCounter = 0;
|
||||
Wt::WLineEdit* searchEdit {main->bindNew<Wt::WLineEdit>("search")};
|
||||
searchEdit->setPlaceholderText(Wt::WString::tr("Lms.Explore.Search.search-placeholder"));
|
||||
|
||||
if (isUserAdmin())
|
||||
{
|
||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration"));
|
||||
menuItem->setSelectable(false);
|
||||
|
||||
auto admin = std::make_unique<Wt::WPopupMenu>();
|
||||
auto dbSettings = admin->insertItem(0, Wt::WString::tr("Lms.Admin.Database.database"));
|
||||
dbSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/database"));
|
||||
dbSettings->setSelectable(false);
|
||||
|
||||
auto usersSettings = admin->insertItem(1, Wt::WString::tr("Lms.Admin.Users.users"));
|
||||
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
|
||||
usersSettings->setSelectable(false);
|
||||
|
||||
menuItem->setMenu(std::move(admin));
|
||||
}
|
||||
|
||||
{
|
||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.Settings.settings"));
|
||||
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/settings"));
|
||||
menuItem->setSelectable(false);
|
||||
}
|
||||
{
|
||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout"));
|
||||
menuItem->setSelectable(true);
|
||||
menuItem->triggered().connect(this, &LmsApplication::handleUserLoggedOut);
|
||||
main->setCondition("if-is-admin", true);
|
||||
main->bindNew<Wt::WAnchor>("database", Wt::WLink {Wt::LinkType::InternalPath, "/admin/database"}, Wt::WString::tr("Lms.Admin.Database.menu-database"));
|
||||
main->bindNew<Wt::WAnchor>("users", Wt::WLink {Wt::LinkType::InternalPath, "/admin/users"}, Wt::WString::tr("Lms.Admin.Users.menu-users"));
|
||||
}
|
||||
|
||||
// Contents
|
||||
@@ -506,11 +468,22 @@ LmsApplication::createHome()
|
||||
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
|
||||
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
||||
|
||||
Explore* explore = mainStack->addNew<Explore>();
|
||||
Explore* explore = mainStack->addNew<Explore>(filters);
|
||||
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
|
||||
mainStack->addNew<PlayHistory>();
|
||||
auto* search {mainStack->addNew<SearchView>(filters)};
|
||||
mainStack->addNew<SettingsView>();
|
||||
|
||||
searchEdit->enterPressed().connect([=]
|
||||
{
|
||||
setInternalPath("/search", true);
|
||||
});
|
||||
|
||||
searchEdit->textInput().connect([=]
|
||||
{
|
||||
setInternalPath("/search", true);
|
||||
search->refreshView(searchEdit->text().toUTF8());
|
||||
});
|
||||
|
||||
// Admin stuff
|
||||
if (isUserAdmin())
|
||||
{
|
||||
@@ -603,7 +576,7 @@ LmsApplication::createHome()
|
||||
});
|
||||
|
||||
// Events from Application group
|
||||
_events.appOpen.connect([=] (LmsApplicationInfo)
|
||||
_events.appOpen.connect([=]
|
||||
{
|
||||
// Only one active session by user
|
||||
if (!LmsApp->isUserDemo())
|
||||
@@ -670,8 +643,9 @@ LmsApplication::notifyMsg(MsgType type, const Wt::WString& message, std::chrono:
|
||||
"message: '" << StringUtils::jsEscape(message.toUTF8()) << "'"
|
||||
"},{"
|
||||
"type: '" << msgTypeToString(type) << "',"
|
||||
"placement: {from: 'top', align: 'center'},"
|
||||
"placement: {from: 'bottom', align: 'right'},"
|
||||
"timer: 250,"
|
||||
"offset: {x: 20, y: 80},"
|
||||
"delay: " << duration.count() << ""
|
||||
"});";
|
||||
|
||||
|
||||
+3
-26
@@ -28,43 +28,20 @@ LmsTheme::LmsTheme(Database::User::UITheme theme)
|
||||
: _theme {theme}
|
||||
{
|
||||
setVersion(Wt::BootstrapVersion::v3);
|
||||
setResponsive(true);
|
||||
applyMetaHeader();
|
||||
}
|
||||
|
||||
void
|
||||
LmsTheme::applyMetaHeader() const
|
||||
{
|
||||
switch (_theme)
|
||||
{
|
||||
case Database::User::UITheme::Dark:
|
||||
LmsApp->addMetaHeader("theme-color", "#303030");
|
||||
break;
|
||||
case Database::User::UITheme::Light:
|
||||
LmsApp->removeMetaHeader(Wt::MetaHeaderType::Meta, "theme-color");
|
||||
break;
|
||||
}
|
||||
setResponsive(true); // Should be not necessary since applied via head-matter
|
||||
}
|
||||
|
||||
void
|
||||
LmsTheme::setTheme(Database::User::UITheme theme)
|
||||
{
|
||||
// Hack, use the application interface directly since changing theme is not allowed
|
||||
|
||||
const auto currentStyleSheets {getStyleSheets(_theme)};
|
||||
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Removing css file '" << it->url() << "'";
|
||||
LmsApp->removeStyleSheet(*it);
|
||||
}
|
||||
|
||||
_theme = theme;
|
||||
for (const auto& styleSheet : getStyleSheets(theme))
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Adding css file '" << styleSheet.url() << "'";
|
||||
LmsApp->useStyleSheet(styleSheet);
|
||||
}
|
||||
applyMetaHeader();
|
||||
}
|
||||
|
||||
std::vector<Wt::WLinkedCssStyleSheet>
|
||||
@@ -86,8 +63,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
||||
case Database::User::UITheme::Dark:
|
||||
return
|
||||
{
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/bootstrap-darkly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-darkly.css"},
|
||||
};
|
||||
@@ -95,8 +72,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
|
||||
case Database::User::UITheme::Light:
|
||||
return
|
||||
{
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/bootstrap-flatly.min.css"},
|
||||
{"resources/themes/bootstrap/3/wt.css"},
|
||||
{"css/lms.css"},
|
||||
{"css/lms-flatly.css"},
|
||||
};
|
||||
|
||||
@@ -37,7 +37,6 @@ class LmsTheme : public Wt::WBootstrapTheme
|
||||
|
||||
private:
|
||||
|
||||
void applyMetaHeader() const;
|
||||
std::vector<Wt::WLinkedCssStyleSheet> styleSheets() const override;
|
||||
static std::vector<Wt::WLink> getStyleSheets(Database::User::UITheme theme);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
@@ -290,6 +291,15 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
|
||||
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
|
||||
wApp->doJavaScript(oss.str());
|
||||
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (track)
|
||||
Database::TrackListEntry::create(LmsApp->getDbSession(), track, LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession()));
|
||||
|
||||
};
|
||||
|
||||
_trackIdLoaded = trackId;
|
||||
trackLoaded.emit(*_trackIdLoaded);
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "PlayHistoryView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace UserInterface;
|
||||
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(Database::Track::pointer track)
|
||||
{
|
||||
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.PlayHistory.template.entry"));
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
auto artists {track->getArtists()};
|
||||
auto release = track->getRelease();
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayHistory.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
if (release)
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(track.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
PlayHistory::PlayHistory()
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.PlayHistory.template")}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
|
||||
|
||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||
_showMore->setHidden(true);
|
||||
|
||||
_showMore->clicked().connect([=]
|
||||
{
|
||||
addSome();
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (track)
|
||||
{
|
||||
Database::TrackListEntry::create(LmsApp->getDbSession(), track, LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession()));
|
||||
_entriesContainer->insertWidget(0, createEntry(track));
|
||||
}
|
||||
});
|
||||
|
||||
addSome();
|
||||
}
|
||||
|
||||
void
|
||||
PlayHistory::addSome()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::TrackList::pointer trackList {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())};
|
||||
auto trackEntries {trackList->getEntriesReverse(_entriesContainer->count(), 50)};
|
||||
for (const auto& trackEntry : trackEntries)
|
||||
_entriesContainer->addWidget(createEntry(trackEntry->getTrack()));
|
||||
|
||||
_showMore->setHidden(static_cast<std::size_t>(_entriesContainer->count()) >= trackList->getCount());
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -32,12 +32,12 @@ class ValueStringModel : public Wt::WStringListModel
|
||||
{
|
||||
public:
|
||||
|
||||
T getValue(std::size_t row)
|
||||
T getValue(std::size_t row) const
|
||||
{
|
||||
return Wt::cpp17::any_cast<T>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::User));
|
||||
}
|
||||
|
||||
Wt::WString getString(std::size_t row)
|
||||
Wt::WString getString(std::size_t row) const
|
||||
{
|
||||
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "ArtistInfoView.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "ArtistLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ArtistInfo::ArtistInfo()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistInfo.template"))
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_similarArtistsContainer = bindNew<Wt::WContainerWidget>("similar-artists");
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
refresh();
|
||||
}));
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect([=]
|
||||
{
|
||||
refresh();
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
ArtistInfo::refresh()
|
||||
{
|
||||
_similarArtistsContainer->clear();
|
||||
|
||||
if (!wApp->internalPathMatches("/artist/"))
|
||||
return;
|
||||
|
||||
auto artistId = StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/artist/"));
|
||||
if (!artistId)
|
||||
return;
|
||||
|
||||
const std::vector<Database::IdType> artistsIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
for (Database::IdType artistId : artistsIds)
|
||||
{
|
||||
Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
||||
if (!artist)
|
||||
continue;
|
||||
|
||||
_similarArtistsContainer->addNew<ArtistLink>(artist);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ArtistInfo : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
ArtistInfo();
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Wt::WContainerWidget* _similarArtistsContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -16,20 +16,32 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ArtistLink.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ArtistLink::ArtistLink(Database::Artist::pointer artist)
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistLink.template"))
|
||||
namespace UserInterface::ArtistListHelpers
|
||||
{
|
||||
bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
createEntry(const Wt::Dbo::ptr<Database::Artist>& artist)
|
||||
{
|
||||
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
|
||||
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
createEntrySmall(const Wt::Dbo::ptr<Database::Artist>& artist)
|
||||
{
|
||||
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry-small"))};
|
||||
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,19 +19,20 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace Database{
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ArtistLink : public Wt::WTemplate
|
||||
namespace UserInterface::ArtistListHelpers
|
||||
{
|
||||
public:
|
||||
ArtistLink(Wt::Dbo::ptr<Database::Artist> artist);
|
||||
|
||||
};
|
||||
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Artist>& artist);
|
||||
std::unique_ptr<Wt::WTemplate> createEntrySmall(const Wt::Dbo::ptr<Database::Artist>& artist);
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "Filters.hpp"
|
||||
@@ -42,22 +43,26 @@ using namespace Database;
|
||||
namespace UserInterface {
|
||||
|
||||
Artist::Artist(Filters* filters)
|
||||
: _filters(filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artist.template")}
|
||||
, _filters(filters)
|
||||
{
|
||||
wApp->internalPathChanged().connect([=]
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
LmsApp->internalPathChanged().connect([=]
|
||||
{
|
||||
refresh();
|
||||
refreshView();
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
filters->updated().connect([=] {
|
||||
refresh();
|
||||
filters->updated().connect([=]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
Artist::refresh()
|
||||
Artist::refreshView()
|
||||
{
|
||||
if (!wApp->internalPathMatches("/artist/"))
|
||||
return;
|
||||
@@ -68,16 +73,17 @@ Artist::refresh()
|
||||
if (!artistId)
|
||||
return;
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const std::vector<Database::IdType> similarArtistIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Artist::pointer artist = Database::Artist::getById(LmsApp->getDbSession(), *artistId);
|
||||
if (!artist)
|
||||
throw ArtistNotFoundException {*artistId};
|
||||
|
||||
Wt::WTemplate* t {addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template"))};
|
||||
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
refreshSimilarArtists(similarArtistIds);
|
||||
|
||||
Wt::WContainerWidget* clusterContainers = t->bindNew<Wt::WContainerWidget>("clusters");
|
||||
Wt::WContainerWidget* clusterContainers = bindNew<Wt::WContainerWidget>("clusters");
|
||||
|
||||
{
|
||||
auto clusterTypes = ScanSettings::get(LmsApp->getDbSession())->getClusterTypes();
|
||||
@@ -97,9 +103,9 @@ Artist::refresh()
|
||||
}
|
||||
}
|
||||
|
||||
t->bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
||||
bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
||||
{
|
||||
Wt::WText* playBtn = t->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
@@ -108,7 +114,7 @@ Artist::refresh()
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WText* addBtn = t->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
@@ -116,70 +122,92 @@ Artist::refresh()
|
||||
});
|
||||
}
|
||||
|
||||
Wt::WContainerWidget* releasesContainer = t->bindNew<Wt::WContainerWidget>("releases");
|
||||
Wt::WContainerWidget* releasesContainer = bindNew<Wt::WContainerWidget>("releases");
|
||||
|
||||
auto releases = artist->getReleases(_filters->getClusterIds());
|
||||
|
||||
for (auto release : releases)
|
||||
for (const auto& release : releases)
|
||||
{
|
||||
auto releaseId = release.id();
|
||||
releasesContainer->addWidget(createRelease(artist, release));
|
||||
}
|
||||
}
|
||||
|
||||
Wt::WTemplate* entry = releasesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry"));
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
Artist::createRelease(const Database::Artist::pointer& artist, const Release::pointer& release)
|
||||
{
|
||||
auto releaseId = release.id();
|
||||
|
||||
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.entry"));
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
|
||||
entry->bindWidget("name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
auto artists {release->getReleaseArtists()};
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())};
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1 && !isSameArtist)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
|
||||
std::optional<int> year {release->getReleaseYear()};
|
||||
if (year)
|
||||
{
|
||||
entry->setCondition("if-has-year", true);
|
||||
entry->bindInt("year", *year);
|
||||
|
||||
std::optional<int> originalYear {release->getReleaseYear(true)};
|
||||
if (originalYear && *originalYear != *year)
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
anchor->setImage(std::move(cover));
|
||||
entry->setCondition("if-has-orig-year", true);
|
||||
entry->bindInt("orig-year", *originalYear);
|
||||
}
|
||||
}
|
||||
|
||||
entry->bindWidget("name", LmsApplication::createReleaseAnchor(release));
|
||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
releasesPlay.emit({releaseId});
|
||||
});
|
||||
|
||||
auto artists {release->getReleaseArtists()};
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releasesAdd.emit({releaseId});
|
||||
});
|
||||
|
||||
bool isSameArtist {(std::find(std::cbegin(artists), std::cend(artists), artist) != artists.end())};
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1 && !isSameArtist)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
void
|
||||
Artist::refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId)
|
||||
{
|
||||
auto* similarArtistsContainer {bindNew<Wt::WContainerWidget>("similar-artists")};
|
||||
|
||||
std::optional<int> year {release->getReleaseYear()};
|
||||
if (year)
|
||||
{
|
||||
entry->setCondition("if-has-year", true);
|
||||
entry->bindInt("year", *year);
|
||||
for (Database::IdType artistId : similarArtistsId)
|
||||
{
|
||||
const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
||||
if (!similarArtist)
|
||||
continue;
|
||||
|
||||
std::optional<int> originalYear {release->getReleaseYear(true)};
|
||||
if (originalYear && *originalYear != *year)
|
||||
{
|
||||
entry->setCondition("if-has-orig-year", true);
|
||||
entry->bindInt("orig-year", *originalYear);
|
||||
}
|
||||
}
|
||||
|
||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
releasesPlay.emit({releaseId});
|
||||
});
|
||||
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releasesAdd.emit({releaseId});
|
||||
});
|
||||
similarArtistsContainer->addWidget(ArtistListHelpers::createEntrySmall(similarArtist));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <memory>
|
||||
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Release;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters;
|
||||
|
||||
class Artist : public Wt::WContainerWidget
|
||||
class Artist : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
Artist(Filters* filters);
|
||||
@@ -40,9 +48,12 @@ class Artist : public Wt::WContainerWidget
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void refreshView();
|
||||
void refreshSimilarArtists(const std::vector<Database::IdType>& similarArtistsId);
|
||||
|
||||
Filters* _filters;
|
||||
std::unique_ptr<Wt::WTemplate> createRelease(const Wt::Dbo::ptr<Database::Artist>& artist, const Wt::Dbo::ptr<Database::Release>& release);
|
||||
|
||||
Filters* _filters {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "ArtistsInfoView.hpp"
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "ArtistLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ArtistsInfo::ArtistsInfo()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistsInfo.template"))
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect([=]
|
||||
{
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
refreshMostPlayed();
|
||||
refreshRecentlyAdded();
|
||||
}
|
||||
|
||||
void
|
||||
ArtistsInfo::refreshRecentlyAdded()
|
||||
{
|
||||
auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6);
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const std::vector<Database::Artist::pointer> artists {Artist::getLastWritten(LmsApp->getDbSession(), after, 5)};
|
||||
|
||||
_recentlyAddedContainer->clear();
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
_recentlyAddedContainer->addNew<ArtistLink>(artist);
|
||||
}
|
||||
|
||||
void
|
||||
ArtistsInfo::refreshMostPlayed()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const std::vector<Database::Artist::pointer> artists {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopArtists(5)};
|
||||
|
||||
_mostPlayedContainer->clear();
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
_mostPlayedContainer->addNew<ArtistLink>(artist);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ArtistsInfo : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
ArtistsInfo();
|
||||
|
||||
void refreshRecentlyAdded();
|
||||
void refreshMostPlayed();
|
||||
|
||||
private:
|
||||
Wt::WContainerWidget* _mostPlayedContainer;
|
||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -21,16 +21,15 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WMenu.h>
|
||||
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Filters.hpp"
|
||||
|
||||
@@ -41,14 +40,29 @@ namespace UserInterface {
|
||||
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLink::Type>>;
|
||||
|
||||
Artists::Artists(Filters* filters)
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Artists.template")),
|
||||
_filters(filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")},
|
||||
_filters {filters}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_search = bindNew<Wt::WLineEdit>("search");
|
||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
||||
_search->textInput().connect(this, &Artists::refresh);
|
||||
{
|
||||
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||
|
||||
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||
{
|
||||
auto* item {menu.addItem(str)};
|
||||
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||
|
||||
if (mode == defaultMode)
|
||||
item->renderSelected(true);
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||
}
|
||||
|
||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||
{
|
||||
@@ -58,51 +72,135 @@ Artists::Artists(Filters* filters)
|
||||
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-releaseartist"), TrackArtistLink::Type::ReleaseArtist);
|
||||
_linkType->setModel(linkTypeModel);
|
||||
}
|
||||
_linkType->changed().connect(this, &Artists::refresh);
|
||||
_linkType->changed().connect([this] { refreshView(); });
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("artists");
|
||||
|
||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||
_showMore->clicked().connect(std::bind([=]
|
||||
_showMore->clicked().connect([=]
|
||||
{
|
||||
addSome();
|
||||
}));
|
||||
});
|
||||
|
||||
refresh();
|
||||
refreshView();
|
||||
|
||||
filters->updated().connect(this, &Artists::refresh);
|
||||
filters->updated().connect([this] { refreshView(); });
|
||||
}
|
||||
|
||||
void
|
||||
Artists::refresh()
|
||||
Artists::refreshView()
|
||||
{
|
||||
_container->clear();
|
||||
_randomArtists.clear();
|
||||
addSome();
|
||||
}
|
||||
|
||||
void
|
||||
Artists::refreshView(Mode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
refreshView();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artists::getRandomArtists(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||
|
||||
if (_randomArtists.empty())
|
||||
_randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), linkType, maxItemsPerMode[Mode::Random]);
|
||||
|
||||
{
|
||||
auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())};
|
||||
auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())};
|
||||
|
||||
for (auto it {itBegin}; it != itEnd; ++it)
|
||||
{
|
||||
Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)};
|
||||
if (artist)
|
||||
artists.push_back(artist);
|
||||
}
|
||||
|
||||
moreResults = (itEnd != std::cend(_randomArtists));
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artists::getArtists(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
const std::optional<TrackArtistLink::Type> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||
|
||||
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||
if (modeLimit)
|
||||
{
|
||||
if (range)
|
||||
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||
else
|
||||
range = Range {0, *modeLimit};
|
||||
}
|
||||
|
||||
switch (_mode)
|
||||
{
|
||||
case Mode::Random:
|
||||
artists = getRandomArtists(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
|
||||
->getArtistsReverse(_filters->getClusterIds(),
|
||||
linkType,
|
||||
range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::MostPlayed:
|
||||
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
|
||||
->getTopArtists(_filters->getClusterIds(),
|
||||
linkType,
|
||||
range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyAdded:
|
||||
artists = Artist::getLastWritten(LmsApp->getDbSession(),
|
||||
std::nullopt,
|
||||
_filters->getClusterIds(),
|
||||
linkType,
|
||||
range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::All:
|
||||
artists = Artist::getByFilter(LmsApp->getDbSession(),
|
||||
_filters->getClusterIds(),
|
||||
{},
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
range, moreResults);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||
moreResults = false;
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
void
|
||||
Artists::addSome()
|
||||
{
|
||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
||||
|
||||
auto clusterIds = _filters->getClusterIds();
|
||||
auto linkModel = static_cast<ArtistLinkModel*>(_linkType->model().get());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const std::vector<Artist::pointer> artists {Artist::getByFilter(LmsApp->getDbSession(),
|
||||
clusterIds,
|
||||
searchKeywords,
|
||||
linkModel->getValue(_linkType->currentIndex()),
|
||||
Artist::SortMethod::BySortName,
|
||||
_container->count(), 20, moreResults)};
|
||||
|
||||
for (const auto& artist : artists)
|
||||
for (const Artist::pointer& artist : getArtists(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
|
||||
{
|
||||
Wt::WTemplate* entry {_container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
|
||||
|
||||
entry->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||
_container->addWidget(ArtistListHelpers::createEntry(artist));
|
||||
}
|
||||
|
||||
_showMore->setHidden(!moreResults);
|
||||
|
||||
@@ -19,15 +19,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters;
|
||||
@@ -37,18 +44,40 @@ class Artists : public Wt::WTemplate
|
||||
public:
|
||||
Artists(Filters* filters);
|
||||
|
||||
Wt::Signal<const std::vector<Database::IdType>&> artistsAdd;
|
||||
Wt::Signal<const std::vector<Database::IdType>&> artistsPlay;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
All
|
||||
};
|
||||
|
||||
void refreshView();
|
||||
void refreshView(Mode mode);
|
||||
void addSome();
|
||||
|
||||
Filters* _filters;
|
||||
Wt::WPushButton* _showMore;
|
||||
Wt::WLineEdit* _search;
|
||||
Wt::WComboBox* _linkType;
|
||||
Wt::WContainerWidget* _container;
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> getArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> getRandomArtists(std::optional<Database::Range> range, bool& moreResults);
|
||||
|
||||
static constexpr Mode defaultMode {Mode::Random};
|
||||
static constexpr std::size_t batchSize {30};
|
||||
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||
{
|
||||
{Mode::Random, batchSize * 4},
|
||||
{Mode::RecentlyPlayed, batchSize * 4},
|
||||
{Mode::RecentlyAdded, batchSize * 2},
|
||||
{Mode::MostPlayed, batchSize * 2},
|
||||
{Mode::All, std::nullopt},
|
||||
};
|
||||
|
||||
Mode _mode {defaultMode};
|
||||
std::vector<Database::IdType> _randomArtists;
|
||||
Filters* _filters {};
|
||||
Wt::WPushButton* _showMore {};
|
||||
Wt::WContainerWidget* _container {};
|
||||
Wt::WComboBox* _linkType {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -30,16 +30,11 @@
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#include "ArtistInfoView.hpp"
|
||||
#include "ArtistsInfoView.hpp"
|
||||
#include "ArtistsView.hpp"
|
||||
#include "ArtistView.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "ReleaseInfoView.hpp"
|
||||
#include "ReleasesInfoView.hpp"
|
||||
#include "ReleasesView.hpp"
|
||||
#include "ReleaseView.hpp"
|
||||
#include "TracksInfoView.hpp"
|
||||
#include "TracksView.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
@@ -77,53 +72,19 @@ handleContentsPathChange(Wt::WStackedWidget* stack)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
handleInfoPathChange(Wt::WStackedWidget* stack)
|
||||
{
|
||||
enum Idx
|
||||
{
|
||||
IdxArtist = 0,
|
||||
IdxArtists,
|
||||
IdxRelease,
|
||||
IdxReleases,
|
||||
IdxTracks,
|
||||
};
|
||||
|
||||
static const std::map<std::string, int> indexes =
|
||||
{
|
||||
{ "/artists", IdxArtists },
|
||||
{ "/artist", IdxArtist },
|
||||
{ "/releases", IdxReleases },
|
||||
{ "/release", IdxRelease },
|
||||
{ "/tracks", IdxTracks },
|
||||
};
|
||||
|
||||
for (auto index : indexes)
|
||||
{
|
||||
if (wApp->internalPathMatches(index.first))
|
||||
{
|
||||
stack->setCurrentIndex(index.second);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Explore::Explore()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.template"))
|
||||
Explore::Explore(Filters* filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template")}
|
||||
, _filters {filters}
|
||||
{
|
||||
addFunction("tr", &Functions::tr);
|
||||
|
||||
_filters = bindNew<Filters>("filters");
|
||||
|
||||
// Contents
|
||||
Wt::WStackedWidget* contentsStack = bindNew<Wt::WStackedWidget>("contents");
|
||||
contentsStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
|
||||
|
||||
auto artists = std::make_unique<Artists>(_filters);
|
||||
artists->artistsAdd.connect(this, &Explore::handleArtistsAdd);
|
||||
artists->artistsPlay.connect(this, &Explore::handleArtistsPlay);
|
||||
contentsStack->addWidget(std::move(artists));
|
||||
|
||||
auto artist = std::make_unique<Artist>(_filters);
|
||||
@@ -150,32 +111,12 @@ Explore::Explore()
|
||||
tracks->tracksPlay.connect(this, &Explore::handleTracksPlay);
|
||||
contentsStack->addWidget(std::move(tracks));
|
||||
|
||||
// Info
|
||||
Wt::WStackedWidget* infoStack = bindNew<Wt::WStackedWidget>("info");
|
||||
|
||||
auto artistInfo = std::make_unique<ArtistInfo>();
|
||||
infoStack->addWidget(std::move(artistInfo));
|
||||
|
||||
auto artistsInfo = std::make_unique<ArtistsInfo>();
|
||||
infoStack->addWidget(std::move(artistsInfo));
|
||||
|
||||
auto releaseInfo = std::make_unique<ReleaseInfo>();
|
||||
infoStack->addWidget(std::move(releaseInfo));
|
||||
|
||||
auto releasesInfo = std::make_unique<ReleasesInfo>();
|
||||
infoStack->addWidget(std::move(releasesInfo));
|
||||
|
||||
auto tracksInfo = std::make_unique<TracksInfo>();
|
||||
infoStack->addWidget(std::move(tracksInfo));
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
wApp->internalPathChanged().connect([=]
|
||||
{
|
||||
handleContentsPathChange(contentsStack);
|
||||
handleInfoPathChange(infoStack);
|
||||
}));
|
||||
});
|
||||
|
||||
handleContentsPathChange(contentsStack);
|
||||
handleInfoPathChange(infoStack);
|
||||
}
|
||||
|
||||
static
|
||||
|
||||
@@ -30,7 +30,7 @@ class Filters;
|
||||
class Explore : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
Explore();
|
||||
Explore(Filters* filters);
|
||||
|
||||
Wt::Signal<std::vector<Database::IdType>> tracksAdd;
|
||||
Wt::Signal<std::vector<Database::IdType>> tracksPlay;
|
||||
@@ -44,7 +44,7 @@ class Explore : public Wt::WTemplate
|
||||
void handleTracksAdd(const std::vector<Database::IdType>& tracksId);
|
||||
void handleTracksPlay(const std::vector<Database::IdType>& tracksId);
|
||||
|
||||
Filters* _filters;
|
||||
Filters* _filters {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -41,7 +41,7 @@ Filters::showDialog()
|
||||
Wt::WComboBox* typeCombo = container->bindNew<Wt::WComboBox>("type");
|
||||
Wt::WComboBox* valueCombo = container->bindNew<Wt::WComboBox>("value");
|
||||
|
||||
Wt::WPushButton* addBtn = container->bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.add"));
|
||||
Wt::WPushButton* addBtn = container->bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Explore.add-filter"));
|
||||
addBtn->clicked().connect(dialog.get(), &Wt::WDialog::accept);
|
||||
|
||||
Wt::WPushButton* cancelBtn = container->bindNew<Wt::WPushButton>("cancel-btn", Wt::WString::tr("Lms.cancel"));
|
||||
@@ -155,12 +155,12 @@ Filters::add(Database::IdType clusterId)
|
||||
}
|
||||
|
||||
Filters::Filters()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.template.filters"))
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.template.filters")}
|
||||
{
|
||||
addFunction("tr", &Functions::tr);
|
||||
|
||||
// Filters
|
||||
Wt::WPushButton *addFilterBtn = bindNew<Wt::WPushButton>("add-filter", Wt::WText::tr("Lms.add"));
|
||||
Wt::WPushButton *addFilterBtn = bindNew<Wt::WPushButton>("add-filter", Wt::WText::tr("Lms.Explore.add-filter"));
|
||||
addFilterBtn->clicked().connect(this, &Filters::showDialog);
|
||||
|
||||
_filters = bindNew<Wt::WContainerWidget>("clusters");
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "ReleaseInfoView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "ReleaseLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ReleaseInfo::ReleaseInfo()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseInfo.template"))
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_similarReleasesContainer = bindNew<Wt::WContainerWidget>("similar-releases");
|
||||
|
||||
wApp->internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
refresh();
|
||||
}));
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect([=]
|
||||
{
|
||||
refresh();
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void
|
||||
ReleaseInfo::refresh()
|
||||
{
|
||||
_similarReleasesContainer->clear();
|
||||
setCondition("if-has-copyright-or-copyright-url", false);
|
||||
setCondition("if-has-copyright-url", false);
|
||||
setCondition("if-has-copyright", false);
|
||||
|
||||
if (!wApp->internalPathMatches("/release/"))
|
||||
return;
|
||||
|
||||
auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
||||
if (!releaseId)
|
||||
return;
|
||||
|
||||
const std::vector<Database::IdType> releasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)};
|
||||
if (!release)
|
||||
return;
|
||||
|
||||
std::optional<std::string> copyright {release->getCopyright()};
|
||||
std::optional<std::string> copyrightURL {release->getCopyrightURL()};
|
||||
|
||||
setCondition("if-has-copyright-or-copyright-url", copyright || copyrightURL);
|
||||
|
||||
if (copyrightURL)
|
||||
{
|
||||
setCondition("if-has-copyright-url", true);
|
||||
|
||||
Wt::WLink link {*copyrightURL};
|
||||
link.setTarget(Wt::LinkTarget::NewWindow);
|
||||
|
||||
Wt::WAnchor* anchor {bindNew<Wt::WAnchor>("copyright-url", link)};
|
||||
anchor->setTextFormat(Wt::TextFormat::XHTML);
|
||||
anchor->setText(Wt::WString::tr("Lms.Explore.Release.template.link-btn"));
|
||||
}
|
||||
|
||||
if (copyright)
|
||||
{
|
||||
setCondition("if-has-copyright", true);
|
||||
bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
std::vector<Database::Release::pointer> similarReleases;
|
||||
for (Database::IdType id : releasesIds)
|
||||
{
|
||||
Database::Release::pointer similarRelease {Database::Release::getById(LmsApp->getDbSession(), id)};
|
||||
|
||||
if (similarRelease)
|
||||
similarReleases.emplace_back(similarRelease);
|
||||
}
|
||||
|
||||
for (const Database::Release::pointer& similarRelease : similarReleases)
|
||||
_similarReleasesContainer->addNew<ReleaseLink>(similarRelease);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ReleaseInfo : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
ReleaseInfo();
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Wt::WContainerWidget* _similarReleasesContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "ReleaseLink.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ReleaseLink::ReleaseLink(Database::Release::pointer release)
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseLink.template"))
|
||||
{
|
||||
bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
Wt::WAnchor* anchor = bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
||||
cover->setStyleClass("Lms-cover-smaller");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
auto artists = release->getReleaseArtists();
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
setCondition("if-has-artist", true);
|
||||
bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
setCondition("if-has-artist", true);
|
||||
bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "ReleaseListHelpers.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface::ReleaseListHelpers
|
||||
{
|
||||
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
createEntrySmall(const Database::Release::pointer& release)
|
||||
{
|
||||
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry-small"))};
|
||||
|
||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
||||
cover->setStyleClass("Lms-cover-smaller");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
auto artists = release->getReleaseArtists();
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,19 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace Database {
|
||||
namespace Database
|
||||
{
|
||||
class Release;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ReleaseLink : public Wt::WTemplate
|
||||
namespace UserInterface::ReleaseListHelpers
|
||||
{
|
||||
public:
|
||||
ReleaseLink(Wt::Dbo::ptr<Database::Release>);
|
||||
};
|
||||
|
||||
std::unique_ptr<Wt::WTemplate> createEntrySmall(const Wt::Dbo::ptr<Database::Release>& release);
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -28,16 +28,16 @@
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -45,53 +45,60 @@ using namespace Database;
|
||||
namespace UserInterface {
|
||||
|
||||
Release::Release(Filters* filters)
|
||||
: _filters(filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Release.template")}
|
||||
, _filters(filters)
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
wApp->internalPathChanged().connect([=]()
|
||||
{
|
||||
refresh();
|
||||
refreshView();
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
filters->updated().connect([=]
|
||||
{
|
||||
refresh();
|
||||
refreshView();
|
||||
});
|
||||
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
Release::refresh()
|
||||
Release::refreshView()
|
||||
{
|
||||
if (!wApp->internalPathMatches("/release/"))
|
||||
return;
|
||||
|
||||
clear();
|
||||
auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
||||
|
||||
const auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
||||
if (!releaseId)
|
||||
return;
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const std::vector<Database::IdType> similarReleasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)};
|
||||
if (!release)
|
||||
throw ReleaseNotFoundException {*releaseId};
|
||||
|
||||
Wt::WTemplate* t {addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template"))};
|
||||
refreshCopyright(release);
|
||||
refreshSimilarReleases(similarReleasesIds);
|
||||
|
||||
t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
std::optional<int> year {release->getReleaseYear()};
|
||||
if (year)
|
||||
{
|
||||
t->setCondition("if-has-year", true);
|
||||
t->bindInt("year", *year);
|
||||
setCondition("if-has-year", true);
|
||||
bindInt("year", *year);
|
||||
|
||||
std::optional<int> originalYear {release->getReleaseYear(true)};
|
||||
if (originalYear && *originalYear != *year)
|
||||
{
|
||||
t->setCondition("if-has-orig-year", true);
|
||||
t->bindInt("orig-year", *originalYear);
|
||||
setCondition("if-has-orig-year", true);
|
||||
bindInt("orig-year", *originalYear);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,22 +111,22 @@ Release::refresh()
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
t->setCondition("if-has-artist", true);
|
||||
t->bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
setCondition("if-has-artist", true);
|
||||
bindNew<Wt::WText>("artist", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
t->setCondition("if-has-artist", true);
|
||||
t->bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
||||
setCondition("if-has-artist", true);
|
||||
bindWidget("artist", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WImage* cover {t->bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)))};
|
||||
Wt::WImage* cover {bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)))};
|
||||
cover->setStyleClass("Lms-cover-large");
|
||||
}
|
||||
|
||||
Wt::WContainerWidget* clusterContainers {t->bindNew<Wt::WContainerWidget>("clusters")};
|
||||
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
|
||||
{
|
||||
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
|
||||
const auto clusterGroups {release->getClusterGroups(clusterTypes, 3)};
|
||||
@@ -139,7 +146,7 @@ Release::refresh()
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WText* playBtn {t->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
releasesPlay.emit({*releaseId});
|
||||
@@ -147,14 +154,14 @@ Release::refresh()
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WText* addBtn {t->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releasesAdd.emit({*releaseId});
|
||||
});
|
||||
}
|
||||
|
||||
Wt::WContainerWidget* rootContainer {t->bindNew<Wt::WContainerWidget>("container")};
|
||||
Wt::WContainerWidget* rootContainer {bindNew<Wt::WContainerWidget>("container")};
|
||||
|
||||
const bool variousArtists {release->hasVariousArtists()};
|
||||
const auto totalDisc {release->getTotalDisc()};
|
||||
@@ -244,5 +251,47 @@ Release::refresh()
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Release::refreshCopyright(const Database::Release::pointer& release)
|
||||
{
|
||||
std::optional<std::string> copyright {release->getCopyright()};
|
||||
std::optional<std::string> copyrightURL {release->getCopyrightURL()};
|
||||
|
||||
setCondition("if-has-copyright-or-copyright-url", copyright || copyrightURL);
|
||||
|
||||
if (copyrightURL)
|
||||
{
|
||||
setCondition("if-has-copyright-url", true);
|
||||
|
||||
Wt::WLink link {*copyrightURL};
|
||||
link.setTarget(Wt::LinkTarget::NewWindow);
|
||||
|
||||
Wt::WAnchor* anchor {bindNew<Wt::WAnchor>("copyright-url", link)};
|
||||
anchor->setTextFormat(Wt::TextFormat::XHTML);
|
||||
anchor->setText(Wt::WString::tr("Lms.Explore.Release.template.link-btn"));
|
||||
}
|
||||
|
||||
if (copyright)
|
||||
{
|
||||
setCondition("if-has-copyright", true);
|
||||
bindString("copyright", Wt::WString::fromUTF8(*copyright), Wt::TextFormat::Plain);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Release::refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId)
|
||||
{
|
||||
auto* similarReleasesContainer {bindNew<Wt::WContainerWidget>("similar-releases")};
|
||||
|
||||
for (Database::IdType id : similarReleasesId)
|
||||
{
|
||||
Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)};
|
||||
if (!similarRelease)
|
||||
continue;
|
||||
|
||||
similarReleasesContainer->addWidget(ReleaseListHelpers::createEntrySmall(similarRelease));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -20,29 +20,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters;
|
||||
class Release : public Wt::WContainerWidget
|
||||
namespace Database
|
||||
{
|
||||
public:
|
||||
Release(Filters* filters);
|
||||
class Release;
|
||||
}
|
||||
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesAdd;
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
Wt::Signal<const std::vector<Database::IdType>&> tracksAdd;
|
||||
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
||||
class Filters;
|
||||
class Release : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
Release(Filters* filters);
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesAdd;
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||
|
||||
Filters* _filters;
|
||||
};
|
||||
Wt::Signal<const std::vector<Database::IdType>&> tracksAdd;
|
||||
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
void refreshCopyright(const Wt::Dbo::ptr<Database::Release>& release);
|
||||
void refreshSimilarReleases(const std::vector<Database::IdType>& similarReleasesId);
|
||||
|
||||
Filters* _filters {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "ReleasesInfoView.hpp"
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ReleaseLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ReleasesInfo::ReleasesInfo()
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleasesInfo.template"))
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect([=]
|
||||
{
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
refreshRecentlyAdded();
|
||||
refreshMostPlayed();
|
||||
}
|
||||
|
||||
void
|
||||
ReleasesInfo::refreshRecentlyAdded()
|
||||
{
|
||||
auto after = Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6);
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto releases {Release::getLastWritten(LmsApp->getDbSession(), after, 0, 5)};
|
||||
|
||||
_recentlyAddedContainer->clear();
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
_recentlyAddedContainer->addNew<ReleaseLink>(release);
|
||||
}
|
||||
|
||||
void
|
||||
ReleasesInfo::refreshMostPlayed()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto releases {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopReleases(5)};
|
||||
|
||||
_mostPlayedContainer->clear();
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
_mostPlayedContainer->addNew<ReleaseLink>(release);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class ReleasesInfo : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
ReleasesInfo();
|
||||
|
||||
private:
|
||||
void refreshRecentlyAdded();
|
||||
void refreshMostPlayed();
|
||||
|
||||
Wt::WContainerWidget* _mostPlayedContainer;
|
||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -19,44 +19,63 @@
|
||||
|
||||
#include "ReleasesView.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Releases::Releases(Filters* filters)
|
||||
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.Releases.template")),
|
||||
_filters(filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Releases.template")},
|
||||
_filters {filters}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_search = bindNew<Wt::WLineEdit>("search");
|
||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
||||
_search->textInput().connect(this, &Releases::refresh);
|
||||
{
|
||||
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||
|
||||
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||
{
|
||||
auto* item {menu.addItem(str)};
|
||||
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||
|
||||
if (mode == defaultMode)
|
||||
item->renderSelected(true);
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||
}
|
||||
|
||||
Wt::WText* playBtn {bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([this]
|
||||
{
|
||||
releasesPlay.emit(getReleases());
|
||||
releasesPlay.emit(getAllReleases());
|
||||
});
|
||||
Wt::WText* addBtn {bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
addBtn->clicked().connect([this]
|
||||
{
|
||||
releasesAdd.emit(getReleases());
|
||||
releasesAdd.emit(getAllReleases());
|
||||
});
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("releases");
|
||||
@@ -67,95 +86,169 @@ _filters(filters)
|
||||
addSome();
|
||||
});
|
||||
|
||||
refresh();
|
||||
refreshView(defaultMode);
|
||||
|
||||
filters->updated().connect(this, &Releases::refresh);
|
||||
filters->updated().connect([this] { refreshView(); });
|
||||
}
|
||||
|
||||
void
|
||||
Releases::refresh()
|
||||
Releases::refreshView()
|
||||
{
|
||||
_container->clear();
|
||||
_randomReleases.clear();
|
||||
addSome();
|
||||
}
|
||||
|
||||
void
|
||||
Releases::refreshView(Mode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
refreshView();
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
Releases::createEntry(const Release::pointer& release)
|
||||
{
|
||||
const IdType releaseId {release.id()};
|
||||
|
||||
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
auto artists = release->getReleaseArtists();
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
|
||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
releasesPlay.emit({releaseId});
|
||||
});
|
||||
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releasesAdd.emit({releaseId});
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void
|
||||
Releases::addSome()
|
||||
{
|
||||
bool moreResults {};
|
||||
|
||||
const auto releasesId {getReleases(_container->count(), 20, moreResults)};
|
||||
for (const Database::IdType releaseId : releasesId )
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
for (const Release::pointer& release : getReleases(Range {static_cast<std::size_t>(_container->count()), batchSize}, moreResults))
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
|
||||
|
||||
Wt::WTemplate* entry = _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry"));
|
||||
entry->addFunction("tr", Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||
// Some images may not be square
|
||||
cover->setStyleClass("Lms-cover-medium");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
|
||||
auto artists = release->getReleaseArtists();
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
if (artists.size() > 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindNew<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||
}
|
||||
|
||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
releasesPlay.emit({releaseId});
|
||||
});
|
||||
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releasesAdd.emit({releaseId});
|
||||
});
|
||||
_container->addWidget(createEntry(release));
|
||||
}
|
||||
|
||||
_showMore->setHidden(!moreResults);
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Releases::getReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit, bool& moreResults) const
|
||||
std::vector<Database::Release::pointer>
|
||||
Releases::getRandomReleases(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
||||
std::vector<Release::pointer> releases;
|
||||
|
||||
if (_randomReleases.empty())
|
||||
_randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
|
||||
|
||||
{
|
||||
auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())};
|
||||
auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())};
|
||||
|
||||
for (auto it {itBegin}; it != itEnd; ++it)
|
||||
{
|
||||
Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)};
|
||||
if (release)
|
||||
releases.push_back(release);
|
||||
}
|
||||
|
||||
moreResults = (itEnd != std::cend(_randomReleases));
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
std::vector<Database::Release::pointer>
|
||||
Releases::getReleases(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Release::pointer> releases;
|
||||
|
||||
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||
if (modeLimit)
|
||||
{
|
||||
if (range)
|
||||
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||
else
|
||||
range = Range {0, *modeLimit};
|
||||
}
|
||||
|
||||
switch (_mode)
|
||||
{
|
||||
case Mode::Random:
|
||||
releases = getRandomReleases(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getReleasesReverse(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::MostPlayed:
|
||||
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopReleases(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyAdded:
|
||||
releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::All:
|
||||
releases = Release::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
|
||||
break;
|
||||
}
|
||||
|
||||
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||
moreResults = false;
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Releases::getAllReleases()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto releases {Release::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), searchKeywords, offset, limit, moreResults)};
|
||||
bool moreResults;
|
||||
const auto releases {getReleases(std::nullopt, moreResults)};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
res.push_back(release.id());
|
||||
std::vector<IdType> res;
|
||||
res.reserve(releases.size());
|
||||
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release.id(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Releases::getReleases() const
|
||||
{
|
||||
bool moreResults;
|
||||
return getReleases({}, {}, moreResults);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -22,12 +22,16 @@
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Release;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters;
|
||||
@@ -41,16 +45,40 @@ class Releases : public Wt::WTemplate
|
||||
Wt::Signal<const std::vector<Database::IdType>&> releasesPlay;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
All
|
||||
};
|
||||
|
||||
void refreshView();
|
||||
void refreshView(Mode mode);
|
||||
void addSome();
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> getReleases(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Wt::Dbo::ptr<Database::Release>> getRandomReleases(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Database::IdType> getAllReleases();
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Release>& release);
|
||||
|
||||
std::vector<Database::IdType> getReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit, bool& moreResults) const;
|
||||
std::vector<Database::IdType> getReleases() const;
|
||||
static constexpr Mode defaultMode {Mode::Random};
|
||||
static constexpr std::size_t batchSize {20};
|
||||
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||
{
|
||||
{Mode::Random, batchSize * 6},
|
||||
{Mode::RecentlyPlayed, batchSize * 3},
|
||||
{Mode::RecentlyAdded, batchSize * 2},
|
||||
{Mode::MostPlayed, batchSize * 2},
|
||||
{Mode::All, std::nullopt},
|
||||
};
|
||||
|
||||
Filters* _filters;
|
||||
Wt::WPushButton* _showMore;
|
||||
Wt::WLineEdit* _search;
|
||||
Wt::WContainerWidget* _container;
|
||||
Mode _mode {defaultMode};
|
||||
Filters* _filters {};
|
||||
std::vector<Database::IdType> _randomReleases;
|
||||
Wt::WContainerWidget* _container {};
|
||||
Wt::WPushButton* _showMore {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 "SearchView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
static constexpr std::size_t maxEntries {5};
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
static
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
createTrack(const Database::Track::pointer& track)
|
||||
{
|
||||
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Search.template.track-entry"))};
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
auto artists {track->getArtists()};
|
||||
auto release {track->getRelease()};
|
||||
const auto trackId {track.id()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
SearchView::SearchView(Filters* filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
||||
, _filters {filters}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_filters->updated().connect([=]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::refreshView(const std::string& searchText)
|
||||
{
|
||||
_keywords = StringUtils::splitString(searchText, " ");
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::refreshView()
|
||||
{
|
||||
clear();
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
searchArtists();
|
||||
searchReleases();
|
||||
searchTracks();
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::searchArtists()
|
||||
{
|
||||
bool more;
|
||||
const auto artists {Database::Artist::getByFilter(LmsApp->getDbSession(),
|
||||
_filters->getClusterIds(),
|
||||
_keywords,
|
||||
std::nullopt,
|
||||
Database::Artist::SortMethod::BySortName,
|
||||
Database::Range {0, maxEntries}, more)};
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
setCondition("if-artists", true);
|
||||
|
||||
auto* container {bindNew<Wt::WContainerWidget>("artists")};
|
||||
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
container->addWidget(ArtistListHelpers::createEntrySmall(artist));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::searchReleases()
|
||||
{
|
||||
bool more;
|
||||
const auto releases {Database::Release::getByFilter(LmsApp->getDbSession(),
|
||||
_filters->getClusterIds(),
|
||||
_keywords,
|
||||
Database::Range {0, maxEntries}, more)};
|
||||
|
||||
if (!releases.empty())
|
||||
{
|
||||
setCondition("if-releases", true);
|
||||
|
||||
auto* container {bindNew<Wt::WContainerWidget>("releases")};
|
||||
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
container->addWidget(ReleaseListHelpers::createEntrySmall(release));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::searchTracks()
|
||||
{
|
||||
bool more;
|
||||
const auto tracks {Database::Track::getByFilter(LmsApp->getDbSession(),
|
||||
_filters->getClusterIds(),
|
||||
_keywords,
|
||||
Database::Range {0, maxEntries}, more)};
|
||||
|
||||
if (!tracks.empty())
|
||||
{
|
||||
setCondition("if-tracks", true);
|
||||
|
||||
auto* container {bindNew<Wt::WContainerWidget>("tracks")};
|
||||
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
container->addWidget(createTrack(track));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,23 +19,30 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class PlayHistory : public Wt::WTemplate
|
||||
class Filters;
|
||||
|
||||
class SearchView : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
PlayHistory();
|
||||
SearchView(Filters* filters);
|
||||
|
||||
void refreshView(const std::string& searchText);
|
||||
|
||||
private:
|
||||
void refreshView();
|
||||
void searchArtists();
|
||||
void searchReleases();
|
||||
void searchTracks();
|
||||
|
||||
void addSome();
|
||||
|
||||
Wt::WContainerWidget* _entriesContainer;
|
||||
Wt::WPushButton* _showMore;
|
||||
Filters* _filters {};
|
||||
std::vector<std::string> _keywords;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "TracksInfoView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace UserInterface;
|
||||
|
||||
void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointer>& tracks)
|
||||
{
|
||||
for (auto track : tracks)
|
||||
{
|
||||
Wt::WTemplate* entry = container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.TracksInfo.template.entry"));
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
TracksInfo::TracksInfo()
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.TracksInfo.template")}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_mostPlayedContainer = bindNew<Wt::WContainerWidget>("most-played");
|
||||
_recentlyAddedContainer = bindNew<Wt::WContainerWidget>("recently-added");
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect([=]
|
||||
{
|
||||
refreshRecentlyAdded();
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
|
||||
{
|
||||
refreshMostPlayed();
|
||||
});
|
||||
|
||||
refreshMostPlayed();
|
||||
refreshRecentlyAdded();
|
||||
}
|
||||
|
||||
void
|
||||
TracksInfo::refreshRecentlyAdded()
|
||||
{
|
||||
const auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const auto tracks {Track::getLastWritten(LmsApp->getDbSession(), after, 5)};
|
||||
|
||||
_recentlyAddedContainer->clear();
|
||||
addEntries(_recentlyAddedContainer, tracks);
|
||||
}
|
||||
|
||||
void
|
||||
TracksInfo::refreshMostPlayed()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
const auto tracks {LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopTracks(5)};
|
||||
|
||||
_mostPlayedContainer->clear();
|
||||
addEntries(_mostPlayedContainer, tracks);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <Wt/WContainerWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class TracksInfo : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
TracksInfo();
|
||||
|
||||
void refreshRecentlyAdded();
|
||||
void refreshMostPlayed();
|
||||
|
||||
private:
|
||||
Wt::WContainerWidget* _mostPlayedContainer;
|
||||
Wt::WContainerWidget* _recentlyAddedContainer;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
#include "TracksView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -47,144 +49,234 @@ _filters {filters}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_search = bindNew<Wt::WLineEdit>("search");
|
||||
_search->setPlaceholderText(Wt::WString::tr("Lms.Explore.search-placeholder"));
|
||||
_search->textInput().connect(this, &Tracks::refresh);
|
||||
{
|
||||
auto* menu {bindNew<Wt::WMenu>("mode")};
|
||||
|
||||
auto addItem = [this](Wt::WMenu& menu,const Wt::WString& str, Mode mode)
|
||||
{
|
||||
auto* item {menu.addItem(str)};
|
||||
item->clicked().connect([this, mode] { refreshView(mode); });
|
||||
|
||||
if (mode == defaultMode)
|
||||
item->renderSelected(true);
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.all"), Mode::All);
|
||||
}
|
||||
|
||||
Wt::WText* playBtn = bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
tracksPlay.emit(getTracks());
|
||||
tracksPlay.emit(getAllTracks());
|
||||
});
|
||||
|
||||
Wt::WText* addBtn = bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
tracksAdd.emit(getTracks());
|
||||
tracksAdd.emit(getAllTracks());
|
||||
});
|
||||
|
||||
_tracksContainer = bindNew<Wt::WContainerWidget>("tracks");
|
||||
|
||||
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
|
||||
_showMore->clicked().connect([=]
|
||||
_showMore->clicked().connect([this]
|
||||
{
|
||||
addSome();
|
||||
});
|
||||
|
||||
refresh();
|
||||
filters->updated().connect([this]
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
filters->updated().connect(this, &Tracks::refresh);
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
Tracks::refreshView()
|
||||
{
|
||||
_tracksContainer->clear();
|
||||
_randomTracks.clear();
|
||||
addSome();
|
||||
}
|
||||
|
||||
void
|
||||
Tracks::refreshView(Mode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
refreshView();
|
||||
}
|
||||
|
||||
std::vector<Database::Track::pointer>
|
||||
Tracks::getRandomTracks(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Track::pointer> tracks;
|
||||
|
||||
if (_randomTracks.empty())
|
||||
_randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), _filters->getClusterIds(), maxItemsPerMode[Mode::Random]);
|
||||
|
||||
{
|
||||
auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())};
|
||||
auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())};
|
||||
|
||||
for (auto it {itBegin}; it != itEnd; ++it)
|
||||
{
|
||||
const Track::pointer track {Track::getById(LmsApp->getDbSession(), *it)};
|
||||
if (track)
|
||||
tracks.push_back(track);
|
||||
}
|
||||
|
||||
moreResults = (itEnd != std::cend(_randomTracks));
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Tracks::getTracks(std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
std::vector<Track::pointer> tracks;
|
||||
|
||||
const std::optional<std::size_t> modeLimit{maxItemsPerMode[_mode]};
|
||||
if (modeLimit)
|
||||
{
|
||||
if (range)
|
||||
range->limit = std::min(*modeLimit - range->offset, range->offset + range->limit);
|
||||
else
|
||||
range = Range {0, *modeLimit};
|
||||
}
|
||||
|
||||
switch (_mode)
|
||||
{
|
||||
case Mode::Random:
|
||||
tracks = getRandomTracks(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTracksReverse(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::MostPlayed:
|
||||
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTopTracks(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyAdded:
|
||||
tracks = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, _filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::All:
|
||||
tracks = Track::getByFilter(LmsApp->getDbSession(), _filters->getClusterIds(), {}, range, moreResults);
|
||||
break;
|
||||
}
|
||||
|
||||
if (range && modeLimit && (range->offset + range->limit == *modeLimit))
|
||||
moreResults = false;
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Tracks::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size, bool& moreResults)
|
||||
Tracks::getAllTracks()
|
||||
{
|
||||
const auto searchKeywords {StringUtils::splitString(_search->text().toUTF8(), " ")};
|
||||
const auto clusterIds {_filters->getClusterIds()};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const auto tracks {Track::getByFilter(LmsApp->getDbSession(), clusterIds, searchKeywords, offset, size, moreResults)};
|
||||
bool moreResults;
|
||||
const auto tracks {getTracks(std::nullopt, moreResults)};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
res.reserve(tracks.size());
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Database::Track::pointer& track) { return track.id(); });
|
||||
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Tracks::getTracks()
|
||||
std::unique_ptr<Wt::WTemplate>
|
||||
Tracks::createEntry(const Track::pointer& track)
|
||||
{
|
||||
bool moreResults;
|
||||
return getTracks({}, {}, moreResults);
|
||||
}
|
||||
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||
auto* entryPtr {entry.get()};
|
||||
|
||||
void
|
||||
Tracks::refresh()
|
||||
{
|
||||
_tracksContainer->clear();
|
||||
addSome();
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
auto artists {track->getArtists()};
|
||||
auto release {track->getRelease()};
|
||||
const auto trackId {track.id()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
}
|
||||
|
||||
entry->bindString("duration", trackDurationToString(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);
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
tracksPlay.emit({trackId});
|
||||
});
|
||||
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
tracksAdd.emit({trackId});
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
|
||||
{
|
||||
entryPtr->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
||||
});
|
||||
|
||||
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
||||
{
|
||||
if (*trackIdLoaded == trackId)
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void
|
||||
Tracks::addSome()
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
bool moreResults;
|
||||
const std::vector<Database::IdType> trackIds {getTracks(_tracksContainer->count(), 20, moreResults)};
|
||||
|
||||
for (const Database::IdType trackId : trackIds)
|
||||
for (const Track::pointer& track : getTracks(Range {static_cast<std::size_t>(_tracksContainer->count()), batchSize}, moreResults))
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
|
||||
Wt::WTemplate* entry {_tracksContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
auto artists {track->getArtists()};
|
||||
auto release {track->getRelease()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Tracks.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||
cover->setStyleClass("Lms-cover-small");
|
||||
}
|
||||
|
||||
entry->bindString("duration", trackDurationToString(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);
|
||||
playBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
tracksPlay.emit({trackId});
|
||||
}));
|
||||
|
||||
Wt::WText* addBtn = entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
addBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
tracksAdd.emit({trackId});
|
||||
}));
|
||||
|
||||
|
||||
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
|
||||
{
|
||||
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
|
||||
});
|
||||
|
||||
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
|
||||
{
|
||||
if (*trackIdLoaded == trackId)
|
||||
entry->bindString("is-playing", "Lms-entry-playing");
|
||||
}
|
||||
_tracksContainer->addWidget(createEntry(track));
|
||||
}
|
||||
|
||||
_showMore->setHidden(!moreResults);
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Track;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Filters;
|
||||
@@ -40,16 +45,42 @@ class Tracks : public Wt::WTemplate
|
||||
Wt::Signal<const std::vector<Database::IdType>&> tracksPlay;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
All
|
||||
};
|
||||
|
||||
void refreshView();
|
||||
void refreshView(Mode mode);
|
||||
void addSome();
|
||||
|
||||
std::vector<Database::IdType> getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size, bool& moreResults);
|
||||
std::vector<Database::IdType> getTracks();
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const Wt::Dbo::ptr<Database::Track>& track);
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> getRandomTracks(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Wt::Dbo::ptr<Database::Track>> getTracks(std::optional<Database::Range> range, bool& moreResults);
|
||||
std::vector<Database::IdType> getAllTracks();
|
||||
|
||||
Wt::WContainerWidget* _tracksContainer;
|
||||
Wt::WPushButton* _showMore;
|
||||
Wt::WLineEdit* _search;
|
||||
Filters* _filters;
|
||||
static constexpr Mode defaultMode {Mode::Random};
|
||||
static constexpr std::size_t batchSize {20};
|
||||
static inline std::unordered_map<Mode, std::optional<std::size_t>> maxItemsPerMode
|
||||
{
|
||||
{Mode::Random, batchSize * 20},
|
||||
{Mode::RecentlyPlayed, batchSize * 10},
|
||||
{Mode::RecentlyAdded, batchSize * 10},
|
||||
{Mode::MostPlayed, batchSize * 10},
|
||||
{Mode::All, std::nullopt},
|
||||
};
|
||||
|
||||
Mode _mode {defaultMode};
|
||||
Filters* _filters {};
|
||||
std::vector<Database::IdType> _randomTracks;
|
||||
Wt::WContainerWidget* _tracksContainer {};
|
||||
Wt::WPushButton* _showMore {};
|
||||
Wt::WLineEdit* _search {};
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -48,6 +48,11 @@ using namespace Database;
|
||||
throw std::runtime_error {error}; \
|
||||
} \
|
||||
} \
|
||||
catch (Wt::Dbo::Exception& e) \
|
||||
{ \
|
||||
std::cerr << "Caught DBO exception: " <<e.code() << std::endl; \
|
||||
throw; \
|
||||
} \
|
||||
catch (std::exception& e) \
|
||||
{ \
|
||||
std::cerr << "Exception caught: " << e.what() << std::endl; \
|
||||
@@ -318,11 +323,11 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
bool hasMore{};
|
||||
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, {}, {}, hasMore).empty());
|
||||
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -398,23 +403,25 @@ void
|
||||
testSingleArtistSearchByName(Session& session)
|
||||
{
|
||||
ScopedArtist artist {session, "AAA"};
|
||||
ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool more {};
|
||||
CHECK(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more).empty());
|
||||
CHECK(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more).empty());
|
||||
|
||||
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
||||
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
||||
CHECK(artistsByAAA.size() == 1);
|
||||
CHECK(artistsByAAA.front().id() == artist.getId());
|
||||
|
||||
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
||||
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)};
|
||||
CHECK(artistsByZZZ.size() == 1);
|
||||
CHECK(artistsByZZZ.front().id() == artist.getId());
|
||||
|
||||
@@ -660,7 +667,7 @@ testMultipleTracksSingleCluster(Session& session)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyClusterType"};
|
||||
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"};
|
||||
|
||||
for (std::size_t i {}; i < 10; ++i)
|
||||
{
|
||||
@@ -686,6 +693,99 @@ testMultipleTracksSingleCluster(Session& session)
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
testMultipleTracksMultipleClustersTopRelease(Session& session)
|
||||
{
|
||||
ScopedClusterType clusterType {session, "ClusterType"};
|
||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"};
|
||||
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "Cluster2"};
|
||||
ScopedCluster cluster3 {session, clusterType.lockAndGet(), "Cluster3"};
|
||||
ScopedTrack trackA {session, "TrackA"};
|
||||
ScopedTrack trackB {session, "TrackB"};
|
||||
ScopedTrack trackC {session, "TrackC"};
|
||||
ScopedRelease releaseA {session, "ReleaseA"};
|
||||
ScopedRelease releaseB {session, "ReleaseB"};
|
||||
ScopedRelease releaseC {session, "ReleaseC"};
|
||||
|
||||
ScopedUser user {session, "MyUser", User::PasswordHash {}};
|
||||
ScopedTrackList trackList {session, "TrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
cluster1.get().modify()->addTrack(trackA.get());
|
||||
cluster2.get().modify()->addTrack(trackB.get());
|
||||
cluster2.get().modify()->addTrack(trackC.get());
|
||||
cluster3.get().modify()->addTrack(trackC.get());
|
||||
|
||||
trackA.get().modify()->setRelease(releaseA.get());
|
||||
trackB.get().modify()->setRelease(releaseB.get());
|
||||
trackC.get().modify()->setRelease(releaseC.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, trackA.get(), trackList.get());
|
||||
TrackListEntry::create(session, trackB.get(), trackList.get());
|
||||
TrackListEntry::create(session, trackB.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool hasMore;
|
||||
const auto releases{trackList->getTopReleases({}, std::nullopt, hasMore)};
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == releaseB.getId());
|
||||
CHECK(releases[1].id() == releaseA.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool hasMore;
|
||||
auto releases{trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases[0].id() == releaseA.getId());
|
||||
|
||||
releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore);
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases[0].id() == releaseB.getId());
|
||||
|
||||
releases = trackList->getTopReleases({cluster2.getId(), cluster1.getId()}, std::nullopt, hasMore);
|
||||
CHECK(releases.empty());
|
||||
|
||||
releases = trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore);
|
||||
CHECK(releases.empty());
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||
TrackListEntry::create(session, trackC.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool hasMore;
|
||||
auto releases {trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases[0].id() == releaseC.getId());
|
||||
|
||||
releases = trackList->getTopReleases({cluster2.getId()}, std::nullopt, hasMore);
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == releaseC.getId());
|
||||
CHECK(releases[1].id() == releaseB.getId());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
@@ -1120,10 +1220,11 @@ testSingleUser(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool hasMore {};
|
||||
CHECK(user->getPlayedTrackList(session)->getCount() == 0);
|
||||
CHECK(user->getPlayedTrackList(session)->getTopTracks(1).empty());
|
||||
CHECK(user->getPlayedTrackList(session)->getTopArtists(1).empty());
|
||||
CHECK(user->getPlayedTrackList(session)->getTopReleases(1).empty());
|
||||
CHECK(user->getPlayedTrackList(session)->getTopTracks({}, Range {0, 1}, hasMore).empty());
|
||||
CHECK(user->getPlayedTrackList(session)->getTopArtists({}, std::nullopt, Range {0, 1}, hasMore).empty());
|
||||
CHECK(user->getPlayedTrackList(session)->getTopReleases({}, Range {0, 1}, hasMore).empty());
|
||||
CHECK(user->getQueuedTrackList(session)->getCount() == 0);
|
||||
}
|
||||
}
|
||||
@@ -1332,6 +1433,261 @@ testSingleTrackListMultipleTrackMultiClusters(Session& session)
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed(Session& session)
|
||||
{
|
||||
ScopedUser user {session, "MyUser", User::PasswordHash {}};
|
||||
ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||
ScopedClusterType clusterType {session, "MyClusterType"};
|
||||
ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"};
|
||||
ScopedCluster cluster2 {session, clusterType.lockAndGet(), "MyCluster2"};
|
||||
ScopedCluster cluster3 {session, clusterType.lockAndGet(), "MyCluster3"};
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack1"};
|
||||
ScopedArtist artist1 {session, "MyArtist1"};
|
||||
ScopedArtist artist2 {session, "MyArtist2"};
|
||||
ScopedRelease release1 {session, "MyRelease1"};
|
||||
ScopedRelease release2 {session, "MyRelease2"};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLink::Type::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLink::Type::Artist);
|
||||
|
||||
cluster1.get().modify()->addTrack(track1.get());
|
||||
cluster2.get().modify()->addTrack(track2.get());
|
||||
cluster3.get().modify()->addTrack(track1.get());
|
||||
cluster3.get().modify()->addTrack(track2.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
CHECK(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty());
|
||||
CHECK(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty());
|
||||
CHECK(trackList->getTracksReverse({}, std::nullopt, moreResults).empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({}, TrackArtistLink::Type::Artist, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
CHECK(trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty());
|
||||
CHECK(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||
CHECK(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty());
|
||||
|
||||
CHECK(trackList->getArtistsReverse({}, TrackArtistLink::Type::ReleaseArtist, std::nullopt, moreResults).empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track2.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 2);
|
||||
CHECK(artists[0].id() == artist2.getId());
|
||||
CHECK(artists[1].id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == release2.getId());
|
||||
CHECK(releases[1].id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 2);
|
||||
CHECK(tracks[0].id() == track2.getId());
|
||||
CHECK(tracks[1].id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 2);
|
||||
CHECK(artists[0].id() == artist2.getId());
|
||||
CHECK(artists[1].id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == release2.getId());
|
||||
CHECK(releases[1].id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 2);
|
||||
CHECK(tracks[0].id() == track2.getId());
|
||||
CHECK(tracks[1].id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist1.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release1.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist2.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release2.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
TrackListEntry::create(session, track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 2);
|
||||
CHECK(artists[0].id() == artist1.getId());
|
||||
CHECK(artists[1].id() == artist2.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == release1.getId());
|
||||
CHECK(releases[1].id() == release2.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 2);
|
||||
CHECK(tracks[0].id() == track1.getId());
|
||||
CHECK(tracks[1].id() == track2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool moreResults {};
|
||||
const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)};
|
||||
CHECK(artists.size() == 2);
|
||||
// TODO investigate
|
||||
// CHECK(artists[0].id() == artist1.getId());
|
||||
// CHECK(artists[1].id() == artist2.getId());
|
||||
|
||||
const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(releases.size() == 2);
|
||||
CHECK(releases[0].id() == release1.getId());
|
||||
CHECK(releases[1].id() == release2.getId());
|
||||
|
||||
const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)};
|
||||
CHECK(tracks.size() == 2);
|
||||
CHECK(tracks[0].id() == track1.getId());
|
||||
CHECK(tracks[1].id() == track2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
testMultipleTracksMultipleArtistsMultiClusters(Session& session)
|
||||
@@ -1519,72 +1875,72 @@ int main()
|
||||
|
||||
std::cout << "Database test file: '" << tmpFile.string() << "'" << std::endl;
|
||||
|
||||
for (std::size_t i = 0; i < 2; ++i)
|
||||
{
|
||||
Database::Db db {tmpFile};
|
||||
Database::Session session {db};
|
||||
session.prepareTables();
|
||||
Database::Db db {tmpFile};
|
||||
Database::Session session {db};
|
||||
session.prepareTables();
|
||||
|
||||
auto runTest = [&session](const std::string& name, std::function<void(Session&)> testFunc)
|
||||
{
|
||||
std::cout << "Running test '" << name << "'..." << std::endl;
|
||||
testFunc(session);
|
||||
testDatabaseEmpty(session);
|
||||
std::cout << "Running test '" << name << "': SUCCESS" << std::endl;
|
||||
};
|
||||
auto runTest = [&session](const std::string& name, std::function<void(Session&)> testFunc)
|
||||
{
|
||||
std::cout << "Running test '" << name << "'..." << std::endl;
|
||||
testFunc(session);
|
||||
testDatabaseEmpty(session);
|
||||
std::cout << "Running test '" << name << "': SUCCESS" << std::endl;
|
||||
};
|
||||
|
||||
#define RUN_TEST(test) runTest(#test, test)
|
||||
|
||||
// Special test to remove any default created entries
|
||||
RUN_TEST(testRemoveDefaultEntries);
|
||||
// Special test to remove any default created entries
|
||||
RUN_TEST(testRemoveDefaultEntries);
|
||||
|
||||
RUN_TEST(testSingleTrack);
|
||||
RUN_TEST(testSingleArtist);
|
||||
RUN_TEST(testSingleRelease);
|
||||
RUN_TEST(testSingleCluster);
|
||||
RUN_TEST(testSingleTrack);
|
||||
RUN_TEST(testSingleArtist);
|
||||
RUN_TEST(testSingleRelease);
|
||||
RUN_TEST(testSingleCluster);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleArtist);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiRoles);
|
||||
RUN_TEST(testSingleTrackMultiArtists);
|
||||
RUN_TEST(testSingleTrackSingleArtist);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiRoles);
|
||||
RUN_TEST(testSingleTrackMultiArtists);
|
||||
|
||||
RUN_TEST(testSingleArtistSearchByName);
|
||||
RUN_TEST(testMultiArtistsSortMethod);
|
||||
RUN_TEST(testSingleArtistSearchByName);
|
||||
RUN_TEST(testMultiArtistsSortMethod);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleRelease);
|
||||
RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack);
|
||||
RUN_TEST(testSingleTrackSingleRelease);
|
||||
RUN_TEST(testMultiTracksSingleReleaseTotalDiscTrack);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleCluster);
|
||||
RUN_TEST(testMultipleTracksSingleCluster);
|
||||
RUN_TEST(testSingleTrackSingleCluster);
|
||||
RUN_TEST(testMultipleTracksSingleCluster);
|
||||
|
||||
RUN_TEST(testMultipleTracksSingleClusterSimilarity);
|
||||
RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
|
||||
RUN_TEST(testMultipleTracksMultipleClustersTopRelease);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleCluster);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiClusters);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiRolesMultiClusters);
|
||||
RUN_TEST(testMultiTracksSingleArtistMultiClusters);
|
||||
RUN_TEST(testMultiTracksSingleArtistSingleRelease);
|
||||
RUN_TEST(testMultipleTracksSingleClusterSimilarity);
|
||||
RUN_TEST(testMultipleTracksMultipleClustersSimilarity);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtist);
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleCluster);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiClusters);
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiRolesMultiClusters);
|
||||
RUN_TEST(testMultiTracksSingleArtistMultiClusters);
|
||||
RUN_TEST(testMultiTracksSingleArtistSingleRelease);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtistSingleCluster);
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtistMultiClusters);
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtist);
|
||||
|
||||
RUN_TEST(testSingleUser);
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtistSingleCluster);
|
||||
RUN_TEST(testSingleTrackSingleReleaseSingleArtistMultiClusters);
|
||||
|
||||
RUN_TEST(testSingleStarredArtist);
|
||||
RUN_TEST(testSingleStarredRelease);
|
||||
RUN_TEST(testSingleStarredTrack);
|
||||
RUN_TEST(testSingleUser);
|
||||
|
||||
RUN_TEST(testSingleTrackList);
|
||||
RUN_TEST(testSingleTrackListMultipleTrack);
|
||||
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster);
|
||||
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
|
||||
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
|
||||
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
|
||||
RUN_TEST(testSingleStarredArtist);
|
||||
RUN_TEST(testSingleStarredRelease);
|
||||
RUN_TEST(testSingleStarredTrack);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
|
||||
}
|
||||
RUN_TEST(testSingleTrackList);
|
||||
RUN_TEST(testSingleTrackListMultipleTrack);
|
||||
RUN_TEST(testSingleTrackListMultipleTrackSingleCluster);
|
||||
RUN_TEST(testSingleTrackListMultipleTrackMultiClusters);
|
||||
RUN_TEST(testSingleTrackListMultipleTrackMultiClustersRecentlyPlayed);
|
||||
RUN_TEST(testMultipleTracksMultipleArtistsMultiClusters);
|
||||
RUN_TEST(testMultipleTracksMultipleReleasesMultiClusters);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleUserSingleBookmark);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
|
||||
@@ -58,7 +58,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
||||
{
|
||||
using namespace MetaData;
|
||||
|
||||
parser.setClusterTypeNames( {"MOOD", "GENRE"} );
|
||||
parser.setClusterTypeNames( {"ALBUMMOOD", "MOOD", "ALBUMGROUPING", "ALBUMGENRE", "GENRE"} );
|
||||
|
||||
const auto start {std::chrono::steady_clock::now()};
|
||||
std::optional<Track> track {parser.parse(file, true)};
|
||||
|
||||
Reference in New Issue
Block a user