Merge branch 'develop' for release v3.12.0

This commit is contained in:
emeric
2020-04-26 15:41:26 +02:00
52 changed files with 1445 additions and 1137 deletions
+61 -73
View File
@@ -4,81 +4,69 @@
<!--FORMS message blocks-->
<message id="Lms.Admin.Database.template">
<legend>${tr:Lms.Admin.Database.database}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:media-directory}">
${tr:Lms.Admin.Database.path}
</label>
<div class="col-sm-5">
${media-directory}
<div class ="row">
<div class="col-lg-8">
<legend>${tr:Lms.Admin.Database.database}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:media-directory}">
${tr:Lms.Admin.Database.path}
</label>
<div class="col-lg-9">
${media-directory}
${media-directory-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:update-period}">
${tr:Lms.Admin.Database.update-period}
</label>
<div class="col-lg-9">
${update-period}
${update-period-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:update-start-time}">
${tr:Lms.Admin.Database.update-start-time}
</label>
<div class="col-lg-9">
${update-start-time}
${update-start-time-info class="help-block"}
</div>
</div>
</div>
<div class="help-block col-sm-5">
${media-directory-info}
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:recommendation-engine-type}">
${tr:Lms.Admin.Database.recommendation-engine-type}
</label>
<div class="col-lg-9">
${recommendation-engine-type}
${recommendation-engine-type-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:tags}">
${tr:Lms.Admin.Database.tags}
</label>
<div class="col-lg-9">
${tags}
${tags-info class="help-block"}
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-period}">
${tr:Lms.Admin.Database.update-period}
</label>
<div class="col-sm-5">
${update-period}
</div>
<div class="help-block col-sm-5">
${update-period-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-start-time}">
${tr:Lms.Admin.Database.update-start-time}
</label>
<div class="col-sm-5">
${update-start-time}
</div>
<div class="help-block col-sm-5">
${update-start-time-info}
</div>
</div>
</div>
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:recommendation-engine-type}">
${tr:Lms.Admin.Database.recommendation-engine-type}
</label>
<div class="col-sm-5">
${recommendation-engine-type}
</div>
<div class="help-block col-sm-5">
${recommendation-engine-type-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:tags}">
${tr:Lms.Admin.Database.tags}
</label>
<div class="col-sm-5">
${tags}
</div>
<div class="help-block col-sm-5">
${tags-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"}
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"}
</div>
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-sm-7">
<div class="col-lg-8">
<div class="panel panel-info">
<div class="panel-heading">${tr:Lms.Admin.Database.Status.status}</div>
<div class="panel-body">
@@ -92,27 +80,27 @@
<message id="Lms.Admin.Database.Status.template">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.Database.Status.status}
</label>
<div class="col-sm-10">
<div class="col-lg-9">
<div class="well well-sm">
${status}
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
<label class="col-lg-3 control-label">
${tr:Lms.Admin.Database.Status.last-scan}
</label>
<div class="col-sm-10">
<div class="col-lg-9">
<div class="well well-sm">
${last-scan}
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="col-lg-offset-3 col-lg-9">
${btn-report class="btn"}
</div>
</div>
+39 -42
View File
@@ -4,52 +4,49 @@
<message id="Lms.Admin.InitWizard.template">
<div class="container">
<div class="page-header">
<h2>${tr:Lms.Admin.InitWizard.header}</h2>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:admin-login}">
${tr:Lms.login}
</label>
<div class="col-sm-5">
${admin-login}
</div>
<div class="help-block col-sm-5">
${admin-login-info}
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="page-header">
<h2>${tr:Lms.Admin.InitWizard.header}</h2>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:admin-login}">
${tr:Lms.login}
</label>
<div class="col-lg-9">
${admin-login}
${admin-login-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-lg-9">
${password}
${password-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-lg-9">
${password-confirm}
${password-confirm-info class="help-block"}
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${create-btn class="btn-primary"}
</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${create-btn class="btn-primary"}
</div>
</div>
</div>
</div>
</message>
-40
View File
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Admin.Subsonic.template">
<legend>${tr:Lms.Admin.Subsonic.subsonic}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:enable}">
${tr:Lms.Admin.Subsonic.enable-api}
</label>
<div class="col-sm-5">
<div class="checkbox"><label>${enable}</label></div>
</div>
<div class="help-block col-sm-5">
${enable-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:artist-list-mode}">
${tr:Lms.Admin.Subsonic.artist-list-mode}
</label>
<div class="col-sm-5">
${artist-list-mode}
</div>
<div class="help-block col-sm-5">
${artist-list-mode-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn}
</div>
</div>
</div>
</message>
</messages>
+49 -69
View File
@@ -2,78 +2,58 @@
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.User.template">
<div class="page-header">
<h2>${title}</h2>
</div>
<div class="form-horizontal">
${<if-has-last-login>}
<div class="form-group">
<label class="control-label col-sm-2" for="{id:last-login}">
${tr:Lms.Admin.User.last-login}
</label>
<div class="col-sm-5">
<div class="well well-sm">
${last-login}
<div class="row">
<div class="col-lg-8">
<legend>${title}</legend>
<div class="form-horizontal">
${<if-has-last-login>}
<div class="form-group">
<label class="control-label col-lg-3" for="{id:last-login}">
${tr:Lms.Admin.User.last-login}
</label>
<div class="col-lg-9">
<div class="well well-sm">
${last-login}
</div>
</div>
</div>
${</if-has-last-login>}
${<if-has-login>}
<div class="form-group">
<label class="control-label col-lg-3" for="${id:login}">
${tr:Lms.login}
</label>
<div class="col-lg-9">
${login}
${login-info class="help-block"}
</div>
</div>
${</if-has-login>}
<div class="form-group">
<label class="control-label col-lg-3" for="${id:password}">
${tr:Lms.password-new}
</label>
<div class="col-lg-9">
${password}
${password-info class="help-block"}
</div>
</div>
</div>
</div>
${</if-has-last-login>}
${<if-has-login>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:login}">
${tr:Lms.login}
</label>
<div class="col-sm-5">
${login}
</div>
<div class="help-block col-sm-5">
${login-info}
</div>
</div>
${</if-has-login>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password-new}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:audio-transcode-bitrate-limit}">
${tr:Lms.Admin.User.audio-transcode-bitrate-limit}
</label>
<div class="col-sm-5">
<div class="input-group">
${audio-transcode-bitrate-limit}
<span class="input-group-addon">kbps</span>
${<if-demo>}
<div class="form-group">
<div class="col-lg-offset-3 col-lg-3" for="${id:demo}">
<div class="checkbox">
<label>${demo}${tr:Lms.Admin.User.demo-account}</label>
${demo-info class="help-block"}
</div>
</div>
</div>
${</if-demo>}
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${save-btn class="btn-primary"}
</div>
</div>
</div>
<div class="help-block col-sm-5">
${audio-transcode-bitrate-limit-info}
</div>
</div>
${<if-demo>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:demo}">
${tr:Lms.Admin.User.demo-account}
</label>
<div class="col-sm-5">
${demo}
</div>
<div class="help-block col-sm-5">
${demo-info}
</div>
</div>
${</if-demo>}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-btn class="btn-primary"}
</div>
</div>
</div>
</message>
+4 -4
View File
@@ -11,11 +11,11 @@
<message id="Lms.Admin.Users.template.entry">
<div class="row Lms-admin-users-entry Lms-vertical-align">
<div class="col-xs-7 Lms-vertical-align">
${name}${<if-tag>}<span class="badge Lms-admin-users-entry-role">${tag}</span>${</if-tag>}
<div class="col-md-5 col-xs-8 Lms-vertical-align">
${name}${<if-tag>}<span class="label label-info Lms-admin-users-entry-role">${tag}</span>${</if-tag>}
</div>
<div class="col-xs-5 Lms-horizontal-center">
${<if-edit>}${edit-btn class="Lms-admin-users-entry-btn"}${del-btn class="btn-danger Lms-admin-users-entry-btn"}${</if-edit>}
<div class="col-md-3 col-xs-4 Lms-horizontal-center">
${<if-edit>}${edit-btn class="btn-warning btn-sm Lms-admin-users-entry-btn"}${del-btn class="btn-danger btn-sm Lms-admin-users-entry-btn"}${</if-edit>}
</div>
</div>
</message>
+2 -1
View File
@@ -3,7 +3,8 @@
<!--FORMS message blocks-->
<message id="Lms.MediaPlayer.template">
<audio id="lms-mp-audio"></audio>
<audio id="lms-mp-audio">
</audio>
<div class="container">
<div class="Lms-player-seek-container">
<input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0"/>
+25 -17
View File
@@ -68,14 +68,6 @@
<message id="Lms.Admin.Database.Status.status-scheduled">Scheduled on {1}</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scanning {1} of {2} files ({3} %)</message>
<!--Subsonic-->
<message id="Lms.Admin.Subsonic.all-artists">All artists</message>
<message id="Lms.Admin.Subsonic.release-artists">Album artists</message>
<message id="Lms.Admin.Subsonic.enable-api">Enable</message>
<message id="Lms.Admin.Subsonic.artist-list-mode">Artist list mode</message>
<message id="Lms.Admin.Subsonic.need-restart">You must restart the service for this change to take effect</message>
<message id="Lms.Admin.Subsonic.subsonic">Subsonic API</message>
<!--Users-->
<message id="Lms.Admin.Users.add">New user</message>
<message id="Lms.Admin.Users.admin">Admin</message>
@@ -166,20 +158,36 @@
<!--Settings-->
<message id="Lms.Settings.appearance">Appearance</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
<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.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.transcoding">Transcoding</message>
<message id="Lms.Settings.transcoding-bitrate">Transcode bitrate</message>
<message id="Lms.Settings.transcoding-format">Transcode format</message>
<message id="Lms.Settings.transcoding-enable">Enable transcoding</message>
<message id="Lms.Settings.transcoding.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding.mp3">MP3</message>
<message id="Lms.Settings.transcoding.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
<message id="Lms.Settings.replaygain-mode.none">No ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Auto</message>
<message id="Lms.Settings.replaygain-mode.track">Track</message>
<message id="Lms.Settings.replaygain-mode.release">Album</message>
<message id="Lms.Settings.replaygain-preamp">ReplayGain preamp</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain preamp (if no info)</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcoding</message>
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message>
<message id="Lms.Settings.transcode-enable">Enable transcoding</message>
<message id="Lms.Settings.transcode-format">Transcode format</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">Enable transcoding</message>
<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>
+28 -19
View File
@@ -68,14 +68,6 @@
<message id="Lms.Admin.Database.Status.status-scheduled">Planifié le {1}</message>
<message id="Lms.Admin.Database.Status.status-in-progress">Scan de {1} fichiers sur {2} ({3} %)</message>
<!--Subsonic-->
<message id="Lms.Admin.Subsonic.all-artists">Tous les artistes</message>
<message id="Lms.Admin.Subsonic.release-artists">Artistes d'album</message>
<message id="Lms.Admin.Subsonic.enable-api">Activer</message>
<message id="Lms.Admin.Subsonic.artist-list-mode">Mode de listage des artistes</message>
<message id="Lms.Admin.Subsonic.need-restart">Vous devez redémarrer le service pour que cette option soit prise en compte</message>
<message id="Lms.Admin.Subsonic.subsonic">API Subsonic</message>
<!--Users-->
<message id="Lms.Admin.Users.add">Ajouter</message>
<message id="Lms.Admin.Users.admin">Admin</message>
@@ -146,11 +138,11 @@
<!--Playqueue-->
<message id="Lms.PlayQueue.clear">Effacer</message>
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
<message id="Lms.PlayQueue.nb-tracks-playing">
<message id="Lms.PlayQueue.nb-tracks-added">
<plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural>
</message>
<message id="Lms.PlayQueue.nb-tracks-added">
<message id="Lms.PlayQueue.nb-tracks-playing">
<plural case="0">{1} piste ajoutée</plural>
<plural case="1">{1} pistes ajoutées</plural>
</message>
@@ -166,20 +158,36 @@
<!--Settings-->
<message id="Lms.Settings.appearance">Apparence</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
<message id="Lms.Settings.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.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.transcoding">Transcodage</message>
<message id="Lms.Settings.transcoding-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcoding-format">Format du transcodage</message>
<message id="Lms.Settings.transcoding-enable">Activer le transcodage</message>
<message id="Lms.Settings.transcoding.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcoding.mp3">MP3</message>
<message id="Lms.Settings.transcoding.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcoding.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Automatique</message>
<message id="Lms.Settings.replaygain-mode.track">Piste</message>
<message id="Lms.Settings.replaygain-mode.release">Album</message>
<message id="Lms.Settings.replaygain-preamp">Pre-amplification ReplayGain</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si pas d'info)</message>
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.transcode">Transcodage</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcode-enable">Activer le transcodage</message>
<message id="Lms.Settings.transcode-format">Format du transcodage</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">Appliquer le transcodage</message>
<message id="Lms.Settings.transcode-mode.always">Toujours</message>
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Seulement lorsque le format n'est pas pris en charge</message>
<message id="Lms.Settings.transcode-mode.never">Jamais</message>
<message id="Lms.Settings.settings">Paramètres</message>
<message id="Lms.Settings.settings-saved">Paramètres sauvegardés !</message>
@@ -187,4 +195,5 @@
<message id="Wt.WMessageBox.Yes">Oui</message>
<message id="Wt.WMessageBox.No">Non</message>
<message id="Wt.WValidator.Invalid">Ce champ ne peut pas être vide</message>
<message id="Wt.WDoubleValidator.BadRange">Le nombre doit être compris entre {1} et {2}</message>
</messages>
+159 -90
View File
@@ -4,100 +4,169 @@
<!--FORMS message blocks-->
<message id="Lms.Settings.template">
<legend>${tr:Lms.Settings.appearance}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label" for="${id:dark-mode}">
${tr:Lms.Settings.dark-mode}
</label>
<div class="col-sm-5">
<div class="checkbox"><label>${dark-mode}</label></div>
</div>
<div class="col-sm-5 help-block">
${dark-mode-info}
</div>
</div>
</div>
<legend>${tr:Lms.Settings.audio}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label" for="${id:transcoding-enable}">
${tr:Lms.Settings.transcoding-enable}
</label>
<div class="col-sm-5">
<div class="checkbox"><label>${transcoding-enable}</label></div>
</div>
<div class="col-sm-5 help-block">
${transcoding-enable-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:transcoding-format}">
${tr:Lms.Settings.transcoding-format}
</label>
<div class="col-sm-5">
${transcoding-format}
</div>
<div class="help-block col-sm-5">
${transcoding-format-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:transcoding-bitrate}">
${tr:Lms.Settings.transcoding-bitrate}
</label>
<div class="col-sm-5">
<div class="input-group">
${transcoding-bitrate}
<span class="input-group-addon">kbps</span>
<div class="row">
<div class="col-lg-8">
<legend>${tr:Lms.Settings.appearance}</legend>
<div class="form-horizontal">
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
<div class="checkbox">
<label>${dark-mode}${tr:Lms.Settings.dark-mode}</label>
${dark-mode-info clas="help-block"}
</div>
</div>
</div>
</div>
<div class="help-block col-sm-5">
${transcoding-bitrate-info}
<legend>${tr:Lms.Settings.audio}</legend>
<div class="alert alert-info">
${tr:Lms.Settings.audio-settings-are-local}
</div>
</div>
</div>
<legend>${tr:Lms.Settings.change-password}</legend>
<div class="form-horizontal">
${<if-has-old-password>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-old}">
${tr:Lms.password-old}
</label>
<div class="col-sm-5">
${password-old}
</div>
<div class="help-block col-sm-5">
${password-old-info}
</div>
</div>
${</if-has-old-password>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password-new}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:transcode-mode}">
${tr:Lms.Settings.transcode-mode}
</label>
<div class="col-lg-9">
${transcode-mode}
${transcode-mode-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:transcode-format}">
${tr:Lms.Settings.transcode-format}
</label>
<div class="col-lg-9">
${transcode-format}
${transcode-format-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:transcode-bitrate}">
${tr:Lms.Settings.transcode-bitrate}
</label>
<div class="col-lg-9">
<div class="input-group">
${transcode-bitrate}
<span class="input-group-addon">kbps</span>
</div>
${transcode-bitrate-info class="help-block"}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn}
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:replaygain-mode}">
${tr:Lms.Settings.replaygain-mode}
</label>
<div class="col-lg-9">
${replaygain-mode}
${replaygain-mode-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:replaygain-preamp}">
${tr:Lms.Settings.replaygain-preamp}
</label>
<div class="col-lg-9">
<div class="input-group">
${replaygain-preamp}
<span class="input-group-addon">dB</span>
</div>
${replaygain-preamp-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:replaygain-preamp-no-rg-info}">
${tr:Lms.Settings.replaygain-preamp-no-rg-info}
</label>
<div class="col-lg-9">
<div class="input-group">
${replaygain-preamp-no-rg-info}
<span class="input-group-addon">dB</span>
</div>
${replaygain-preamp-no-rg-info-info class="help-block"}
</div>
</div>
</div>
${<if-has-subsonic-api>}
<legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="form-horizontal">
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
<div class="checkbox">
<label>${subsonic-transcode-enable}${tr:Lms.Settings.transcode-enable}</label>
${subsonic-transcode-enable-info class="help-block"}
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:subsonic-transcode-format}">
${tr:Lms.Settings.transcode-format}
</label>
<div class="col-lg-9">
${subsonic-transcode-format}
${subsonic-transcode-format-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:subsonic-transcode-bitrate}">
${tr:Lms.Settings.transcode-bitrate}
</label>
<div class="col-lg-9">
<div class="input-group">
${subsonic-transcode-bitrate}
<span class="input-group-addon">kbps</span>
</div>
${subsonic-transcode-bitrate-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:subsonic-artist-list-mode}">
${tr:Lms.Settings.subsonic-artist-list-mode}
</label>
<div class="col-lg-9">
${subsonic-artist-list-mode}
${subsonic-artist-list-mode-info class="help-block"}
</div>
</div>
</div>
${</if-has-subsonic-api>}
<legend>${tr:Lms.Settings.change-password}</legend>
<div class="form-horizontal">
${<if-has-old-password>}
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:password-old}">
${tr:Lms.password-old}
</label>
<div class="col-lg-9">
${password-old}
${password-old-info class="help-block"}
</div>
</div>
${</if-has-old-password>}
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:password}">
${tr:Lms.password-new}
</label>
<div class="col-lg-9">
${password}
${password-info class="help-block"}
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-lg-9">
${password-confirm}
${password-confirm-info class="help-block"}
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-3 col-lg-9">
${apply-btn class="btn-primary"} ${discard-btn}
</div>
</div>
</div>
</div>
</div>
+3
View File
@@ -34,6 +34,9 @@ deploy-path = "/";
# Acoustic brainz's root API
acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/";
# API
api-subsonic = true;
# Turn on this option to allow the demo account creation/use
demo = false;
+1
View File
@@ -239,6 +239,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
.text-success .Lms-playqueue-btn {
filter: brightness(130%);
filter: drop-shadow(0 0 0.65rem var(--hover-color));
color: var(--hover-color);
}
.Lms-playqueue-entry-controls {
+92 -11
View File
@@ -2,6 +2,13 @@
var LMS = LMS || {};
// Keep in sync with MediaPlayer::TranscodeMode cpp
const TranscodeMode = {
Never: 0,
Always: 1,
IfFormatNotSupported: 2,
}
const Mode = {
Transcode: 1,
File: 2,
@@ -14,8 +21,11 @@ LMS.mediaplayer = function () {
var _elems = {};
var _offset = 0;
var _duration = 0;
var _audioSrc;
var _mode = Mode.File;
var _audioNativeSrc;
var _audioTranscodeSrc;
var _settings = {};
var _audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var _gainNode = _audioCtx.createGain();
var _updateControls = function() {
if (_elems.audio.paused) {
@@ -71,6 +81,17 @@ LMS.mediaplayer = function () {
_setVolume(_elems.volumeslider.value);
}
var _initDefaultSettings = function(defaultSettings) {
if (typeof(Storage) !== "undefined" && localStorage.settings) {
_settings = Object.assign(defaultSettings, JSON.parse(localStorage.settings));
}
else {
_settings = defaultSettings;
}
Wt.emit(_root, "settingsLoaded", JSON.stringify(_settings));
}
var _setVolume = function(volume) {
_elems.lastvolume = _elems.audio.volume;
@@ -99,7 +120,11 @@ LMS.mediaplayer = function () {
}
}
var init = function(root) {
var _setReplayGain = function (replayGain) {
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
}
var init = function(root, defaultSettings) {
_root = root;
_elems.audio = document.getElementById("lms-mp-audio");
@@ -113,31 +138,41 @@ LMS.mediaplayer = function () {
_elems.volume = document.getElementById("lms-mp-volume");
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
var source = _audioCtx.createMediaElementSource(_elems.audio);
source.connect(_gainNode);
_gainNode.connect(_audioCtx.destination);
_elems.playpause.addEventListener("click", function() {
if (_elems.audio.paused) {
if (_elems.audio.hasAttribute("src"))
_elems.audio.play();
if (_elems.audio.firstChild) {
_audioCtx.resume();
_playTrack();
}
}
else
_elems.audio.pause();
});
_elems.previous.addEventListener("click", function() {
_audioCtx.resume();
_requestPreviousTrack();
});
_elems.next.addEventListener("click", function() {
_audioCtx.resume();
_requestNextTrack();
});
_elems.seek.addEventListener("change", function() {
if (!_elems.audio.hasAttribute("src"))
let mode = _getAudioMode();
if (!mode)
return;
let selectedOffset = parseInt(_elems.seek.value, 10);
switch (_mode) {
switch (mode) {
case Mode.Transcode:
_offset = selectedOffset;
_elems.audio.src = _audioSrc + "&offset=" + _offset;
_removeAudioSources();
_addAudioSource(_audioTranscodeSrc + "&offset=" + _offset);
_elems.audio.load();
_elems.audio.currentTime = 0;
_playTrack();
@@ -164,6 +199,7 @@ LMS.mediaplayer = function () {
});
_initVolume();
_initDefaultSettings(defaultSettings);
_elems.volumeslider.addEventListener("input", function() {
_setVolume(_elems.volumeslider.value);
@@ -189,14 +225,50 @@ LMS.mediaplayer = function () {
}
var _removeAudioSources = function() {
while ( _elems.audio.lastElementChild) {
_elems.audio.removeChild( _elems.audio.lastElementChild);
}
}
var _addAudioSource = function(audioSrc) {
let source = document.createElement('source');
source.src = audioSrc;
_elems.audio.appendChild(source);
}
var _getAudioMode = function() {
if (_elems.audio.currentSrc) {
if (_elems.audio.currentSrc.includes("format"))
return Mode.Transcode;
else
return Mode.File;
}
else
return undefined;
}
var loadTrack = function(params, autoplay) {
_offset = 0;
_duration = params.duration;
_audioSrc = params.resource;
_mode = params.mode;
_audioNativeSrc = params.nativeResource;
_audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format;
_elems.seek.max = _duration;
_elems.audio.src = _audioSrc;
_removeAudioSources();
// ! order is important
if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{
_addAudioSource(_audioNativeSrc);
}
if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported)
{
_addAudioSource(_audioTranscodeSrc);
}
_elems.audio.load();
_setReplayGain(params.replayGain);
_elems.curtime.innerHTML = _durationToString(_offset);
_elems.duration.innerHTML = _durationToString(_duration);
@@ -218,10 +290,19 @@ LMS.mediaplayer = function () {
_elems.audio.pause();
}
var setSettings = function(settings) {
_settings = settings;
if (typeof(Storage) !== "undefined") {
localStorage.settings = JSON.stringify(_settings);
}
}
return {
init: init,
loadTrack: loadTrack,
stop: stop,
setSettings: setSettings,
};
}();
+7 -7
View File
@@ -104,35 +104,35 @@ Transcoder::start()
args.emplace_back(std::to_string(_parameters.bitrate));
// Codecs and formats
switch (_parameters.encoding)
switch (_parameters.format)
{
case Encoding::MP3:
case Format::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
case Encoding::OGG_OPUS:
case Format::OGG_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case Encoding::MATROSKA_OPUS:
case Format::MATROSKA_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("matroska");
break;
case Encoding::OGG_VORBIS:
case Format::OGG_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case Encoding::WEBM_VORBIS:
case Format::WEBM_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
@@ -143,7 +143,7 @@ Transcoder::start()
return false;
}
_outputMimeType = encodingToMimetype(_parameters.encoding);
_outputMimeType = formatToMimetype(_parameters.format);
args.emplace_back("pipe:1");
+7 -7
View File
@@ -21,15 +21,15 @@
namespace Av {
const char* encodingToMimetype(Encoding encoding)
const char* formatToMimetype(Format format)
{
switch (encoding)
switch (format)
{
case Encoding::MP3: return "audio/mpeg";
case Encoding::OGG_OPUS: return "audio/opus";
case Encoding::MATROSKA_OPUS: return "audio/x-matroska";
case Encoding::OGG_VORBIS: return "audio/ogg";
case Encoding::WEBM_VORBIS: return "audio/webm";
case Format::MP3: return "audio/mpeg";
case Format::OGG_OPUS: return "audio/opus";
case Format::MATROSKA_OPUS: return "audio/x-matroska";
case Format::OGG_VORBIS: return "audio/ogg";
case Format::WEBM_VORBIS: return "audio/webm";
}
throw AvException {"Invalid encoding"};
+1 -1
View File
@@ -33,7 +33,7 @@ namespace Av {
struct TranscodeParameters
{
Encoding encoding;
Format format;
std::size_t bitrate {128000};
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
std::optional<std::chrono::seconds> offset;
+7 -7
View File
@@ -31,17 +31,17 @@ class AvException : public LmsException
AvException(const std::string& msg) : LmsException(msg) {}
};
enum class Encoding
enum class Format
{
// Values are important and must not be changed
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
MP3 = 0,
OGG_OPUS = 1,
MATROSKA_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
};
const char* encodingToMimetype(Encoding encoding);
const char* formatToMimetype(Format encoding);
}
-1
View File
@@ -11,7 +11,6 @@ add_library(lmsdatabase SHARED
impl/Session.cpp
impl/SessionPool.cpp
impl/SqlQuery.cpp
impl/SubsonicSettings.cpp
impl/Track.cpp
impl/TrackBookmark.cpp
impl/User.cpp
+43 -8
View File
@@ -31,7 +31,6 @@
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/SubsonicSettings.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackArtistLink.hpp"
@@ -41,7 +40,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 19
#define LMS_DATABASE_VERSION 22
using Version = std::size_t;
@@ -198,11 +197,8 @@ CREATE TABLE "release_backup" (
"mbid" text not null
))");
_session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
_session.execute("DROP TABLE release;");
_session.execute("DROP TABLE release");
_session.execute("ALTER TABLE release_backup RENAME TO release");
_session.execute("CREATE INDEX release_name_idx ON release(name)");
_session.execute("CREATE INDEX release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX release_mbid_idx ON release(mbid)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
@@ -217,6 +213,47 @@ CREATE TABLE IF NOT EXISTS "subsonic_settings" (
"artist_list_mode" integer not null
))");
}
else if (version == 19)
{
_session.execute(R"(
CREATE TABLE "user_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"type" integer not null,
"login_name" text not null,
"password_salt" text not null,
"password_hash" text not null,
"last_login" text,
"subsonic_transcode_enable" boolean not null,
"subsonic_transcode_format" integer not null,
"subsonic_transcode_bitrate" integer not null,
"subsonic_artist_list_mode" integer not null,
"ui_theme" integer not null,
"cur_playing_track_pos" integer not null,
"repeat_all" boolean not null,
"radio" boolean not null
))");
_session.execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "}
+ (User::defaultSubsonicTranscodeEnable ? "1" : "0")
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
+ ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
_session.execute("DROP TABLE user");
_session.execute("ALTER TABLE user_backup RENAME TO user");
}
else if (version == 20)
{
_session.execute("DROP TABLE subsonic_settings");
}
else if (version == 21)
{
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else
{
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
@@ -239,7 +276,6 @@ Session::Session(Db& db)
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Release>("release");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<SubsonicSettings>("subsonic_settings");
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
@@ -365,7 +401,6 @@ Session::prepareTables()
auto uniqueTransaction {createUniqueTransaction()};
ScanSettings::init(*this);
SubsonicSettings::init(*this);
}
}
@@ -1,48 +0,0 @@
/*
* 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 "database/SubsonicSettings.hpp"
#include "database/Session.hpp"
namespace Database {
void
SubsonicSettings::init(Session& session)
{
session.checkUniqueLocked();
pointer settings {get(session)};
if (settings)
return;
settings = session.getDboSession().add(std::make_unique<SubsonicSettings>());
}
SubsonicSettings::pointer
SubsonicSettings::get(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().find<SubsonicSettings>();
}
} // ns Database
+25 -4
View File
@@ -38,6 +38,14 @@ _filePath( p.string() )
{
}
std::size_t
Track::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
}
std::vector<Track::pointer>
Track::getAll(Session& session, std::optional<std::size_t> limit)
{
@@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p)
return res;
}
std::vector<std::filesystem::path>
Track::getAllPaths(Session& session)
std::vector<std::pair<IdType, std::filesystem::path>>
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
{
using QueryResultType = std::tuple<IdType, std::string>;
session.checkSharedLocked();
Wt::Dbo::collection<std::string> res = session.getDboSession().query<std::string>("SELECT file_path FROM track");
return std::vector<std::filesystem::path>(res.begin(), res.end());
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
.limit(size ? static_cast<int>(*size) + 1 : -1)
.offset(offset ? static_cast<int>(*offset) : -1);
std::vector<std::pair<IdType, std::filesystem::path>> result;
result.reserve(queryRes.size());
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
[](const QueryResultType& queryResult)
{
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
});
return result;
}
std::vector<Track::pointer>
+6 -62
View File
@@ -70,28 +70,11 @@ AuthToken::getByValue(Session& session, const std::string& value)
static const std::string playedListName {"__played_tracks__"};
static const std::string queuedListName {"__queued_tracks__"};
const std::set<Bitrate>
User::audioTranscodeAllowedBitrates =
{
64000,
96000,
128000,
192000,
320000,
};
User::User()
: _maxAudioTranscodeBitrate {static_cast<int>(*audioTranscodeAllowedBitrates.rbegin())}
{
}
User::User(const std::string& loginName, const PasswordHash& passwordHash)
: User()
: _loginName {loginName}
, _passwordSalt {passwordHash.salt}
, _passwordHash {passwordHash.hash}
{
_loginName = loginName;
_passwordHash = passwordHash.hash;
_passwordSalt = passwordHash.salt;
}
std::vector<User::pointer>
@@ -141,27 +124,10 @@ User::getByLoginName(Session& session, const std::string& name)
}
void
User::setAudioTranscodeBitrate(Bitrate bitrate)
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
{
_audioTranscodeBitrate = std::min(bitrate, static_cast<Bitrate>(_maxAudioTranscodeBitrate));
}
void
User::setMaxAudioTranscodeBitrate(Bitrate requestedBitrate)
{
Bitrate bitrate {*audioTranscodeAllowedBitrates.begin()};
for (auto allowedBitrate : audioTranscodeAllowedBitrates)
{
if (requestedBitrate < allowedBitrate)
break;
bitrate = allowedBitrate;
}
_maxAudioTranscodeBitrate = bitrate;
if (_audioTranscodeBitrate > _maxAudioTranscodeBitrate)
_audioTranscodeBitrate = _maxAudioTranscodeBitrate;
assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend());
_subsonicTranscodeBitrate = bitrate;
}
void
@@ -170,18 +136,6 @@ User::clearAuthTokens()
_authTokens.clear();
}
Bitrate
User::getAudioTranscodeBitrate(void) const
{
return _audioTranscodeBitrate;
}
std::size_t
User::getMaxAudioTranscodeBitrate(void) const
{
return _maxAudioTranscodeBitrate;
}
Wt::Dbo::ptr<TrackList>
User::getPlayedTrackList(Session& session) const
{
@@ -283,16 +237,6 @@ User::getStarredTracks() const
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
}
bool
User::checkBitrate(Database::Bitrate bitrate) const
{
if (audioTranscodeAllowedBitrates.find(bitrate) == std::cend(audioTranscodeAllowedBitrates))
return false;
return static_cast<Bitrate>(_maxAudioTranscodeBitrate) >= bitrate;
}
} // namespace Database
@@ -1,69 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <Wt/Dbo/Dbo.h>
namespace Database {
class Session;
class SubsonicSettings : public Wt::Dbo::Dbo<SubsonicSettings>
{
public:
using pointer = Wt::Dbo::ptr<SubsonicSettings>;
enum class ArtistListMode
{
AllArtists,
ReleaseArtists,
};
static inline constexpr bool defaultSubsonicAPIEnabled {true};
static inline constexpr ArtistListMode defaultArtistListMode {ArtistListMode::AllArtists};
static void init(Session& session);
static pointer get(Session& session);
// Getters
bool isAPIEnabled() const { return _isAPIEnabled; }
ArtistListMode getArtistListMode() const { return _artistListMode; }
// Setters
void setAPIEnabled(bool enabled) { _isAPIEnabled = enabled; }
void setArtistListMode(ArtistListMode mode) {_artistListMode = mode; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _isAPIEnabled, "api_enabled");
Wt::Dbo::field(a, _artistListMode, "artist_list_mode");
}
private:
bool _isAPIEnabled {defaultSubsonicAPIEnabled};
ArtistListMode _artistListMode {defaultArtistListMode};
};
} // namespace Database
+11 -1
View File
@@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo<Track>
Track(const std::filesystem::path& p);
// Find utility functions
static std::size_t getCount(Session& session);
static pointer getByPath(Session& session, const std::filesystem::path& p);
static pointer getById(Session& session, IdType id);
static pointer getByMBID(Session& session, const UUID& MBID);
@@ -74,7 +75,7 @@ class Track : public Wt::Dbo::Dbo<Track>
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<IdType> getAllIds(Session& session);
static std::vector<std::filesystem::path> getAllPaths(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> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
@@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo<Track>
void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; }
void clearArtistLinks();
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
@@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo<Track>
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
@@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
@@ -184,6 +192,8 @@ class Track : public Wt::Dbo::Dbo<Track>
std::string _MBID; // Musicbrainz Identifier
std::string _copyright;
std::string _copyrightURL;
std::optional<float> _trackReplayGain;
std::optional<float> _releaseReplayGain;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
+39 -24
View File
@@ -36,7 +36,7 @@ class Session;
class TrackList;
class Track;
// User selectable audio formats
// User selectable audio file formats
// Do not change values
enum class AudioFormat
{
@@ -113,16 +113,33 @@ class User : public Wt::Dbo::Dbo<User>
Dark = 1,
};
static const std::set<Bitrate> audioTranscodeAllowedBitrates;
// Do not remove values!
static inline const std::set<Bitrate> audioTranscodeAllowedBitrates
{
64000,
96000,
128000,
192000,
320000,
};
// Do not change enum values!
enum class SubsonicArtistListMode
{
AllArtists = 0,
ReleaseArtists = 1,
};
static inline const std::size_t MinNameLength {3};
static inline const std::size_t MaxNameLength {15};
static inline const bool defaultAudioTranscodeEnable {true};
static inline const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS};
static inline const Bitrate defaultAudioTranscodeBitrate {128000};
static inline const bool defaultSubsonicTranscodeEnable {true};
static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS};
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark};
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
User();
User() = default;
User(const std::string& loginName, const PasswordHash& passwordHash);
// utility
@@ -143,27 +160,27 @@ class User : public Wt::Dbo::Dbo<User>
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
void setType(Type type) { _type = type; }
void setAudioTranscodeEnable(bool value) { _audioTranscodeEnable = value; }
void setAudioTranscodeFormat(AudioFormat format) { _audioTranscodeFormat = format; }
void setAudioTranscodeBitrate(Bitrate bitrate);
void setMaxAudioTranscodeBitrate(Bitrate bitrate);
void setSubsonicTranscodeEnable(bool value) { _subsonicTranscodeEnable = value; }
void setSubsonicTranscodeFormat(AudioFormat encoding) { _subsonicTranscodeFormat = encoding; }
void setSubsonicTranscodeBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
// read
bool isAdmin() const { return _type == Type::ADMIN; }
bool isDemo() const { return _type == Type::DEMO; }
bool getAudioTranscodeEnable() const { return _audioTranscodeEnable; }
Bitrate getAudioTranscodeBitrate() const;
AudioFormat getAudioTranscodeFormat() const { return _audioTranscodeFormat; }
Bitrate getMaxAudioTranscodeBitrate() const;
bool getSubsonicTranscodeEnable() const { return _subsonicTranscodeEnable; }
AudioFormat getSubsonicTranscodeFormat() const { return _subsonicTranscodeFormat; }
Bitrate getSubsonicTranscodeBitrate() const { return _subsonicTranscodeBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Wt::Dbo::ptr<TrackList> getPlayedTrackList(Session& session) const;
Wt::Dbo::ptr<TrackList> getQueuedTrackList(Session& session) const;
@@ -184,8 +201,6 @@ class User : public Wt::Dbo::Dbo<User>
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
bool checkBitrate(Bitrate bitrate) const;
template<class Action>
void persist(Action& a)
{
@@ -194,10 +209,10 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login");
Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate");
Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable");
Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate");
Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format");
Wt::Dbo::field(a, _subsonicTranscodeEnable, "subsonic_transcode_enable");
Wt::Dbo::field(a, _subsonicTranscodeFormat, "subsonic_transcode_format");
Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
// User's dynamic data
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
@@ -219,13 +234,13 @@ class User : public Wt::Dbo::Dbo<User>
UITheme _uiTheme {defaultUITheme};
// Admin defined settings
int _maxAudioTranscodeBitrate;
Type _type {Type::REGULAR};
// User defined settings
bool _audioTranscodeEnable {defaultAudioTranscodeEnable};
AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat};
int _audioTranscodeBitrate {defaultAudioTranscodeBitrate};
SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode};
bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable};
AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat};
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
// User's dynamic data (UI)
int _curPlayingTrackPos {}; // Current track position in queue
+37 -3
View File
@@ -19,13 +19,16 @@
#include "metadata/TagLibParser.hpp"
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/id3v2tag.h>
#include <taglib/fileref.h>
#include <taglib/flacfile.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/wavpackfile.h>
#include "utils/Logger.hpp"
#include "utils/String.hpp"
@@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
track.albumReplayGain = StringUtils::readAs<float>(value);
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.trackReplayGain = StringUtils::readAs<float>(value);
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
@@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
TagLib::PropertyMap properties {f.file()->properties()};
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{
if (!apeTag)
return;
for (const auto& [name, values] : apeTag->properties())
{
if (debug)
std::cout << "APE property: '" << name << "'" << std::endl;
if (!properties.contains(name))
properties.insert(name, values);
}
};
// Not that good embedded pictures handling
// WMA
@@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
if (!stringAttributeList.isEmpty())
{
if (debug)
std::cout << "Property: '" << name << "'" << std::endl;
std::cout << "ASF property: '" << name << "'" << std::endl;
properties.insert(name, stringAttributeList);
if (!properties.contains(name))
properties.insert(name, stringAttributeList);
}
}
}
@@ -352,6 +375,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
track.hasCover = true;
}
getAPETags(mp3File->APETag());
}
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())})
{
getAPETags(mpcFile->APETag());
}
// WavPack
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())})
{
getAPETags(wavPackFile->APETag());
}
// FLAC
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())})
@@ -360,7 +394,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true;
}
for(const auto& property : properties)
for (const auto& property : properties)
{
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
@@ -75,6 +75,8 @@ namespace MetaData
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain;
};
class IParser
+44 -12
View File
@@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
// Sortname may have been updated
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
{
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
artist.modify()->setSortName(*artistInfo.sortName);
}
}
@@ -751,6 +750,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
track.modify()->setHasCover(trackInfo->hasCover);
track.modify()->setCopyright(trackInfo->copyright);
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
if (trackInfo->trackReplayGain)
track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain);
if (trackInfo->albumReplayGain)
track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain);
}
void
@@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
void
MediaScanner::removeMissingTracks(ScanStats& stats)
{
std::vector<std::filesystem::path> trackPaths;
static constexpr std::size_t batchSize {50};
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
std::size_t trackCount {};
{
auto transaction {_dbSession.createSharedTransaction()};
trackPaths = Track::getAllPaths(_dbSession);;
trackCount = Track::getCount(_dbSession);
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (const auto& trackPath : trackPaths)
std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
std::vector<IdType> tracksToRemove;
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
{
if (!_running)
return;
trackPaths.clear();
tracksToRemove.clear();
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
{
auto transaction {_dbSession.createSharedTransaction()};
trackPaths = Track::getAllPaths(_dbSession, i, batchSize);
}
for (const auto& [trackId, trackPath] : trackPaths)
{
if (!_running)
return;
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
tracksToRemove.push_back(trackId);
}
if (!tracksToRemove.empty())
{
auto transaction {_dbSession.createUniqueTransaction()};
Track::pointer track {Track::getByPath(_dbSession, trackPath)};
if (track)
for (const IdType trackId : tracksToRemove)
{
track.remove();
stats.deletions++;
Track::pointer track {Track::getById(_dbSession, trackId)};
if (track)
{
track.remove();
stats.deletions++;
}
}
}
notifyInProgressIfNeeded(stats);
if (i == 0)
break;
}
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
}
void
+15 -15
View File
@@ -37,17 +37,17 @@ namespace API::Subsonic::Stream
{
static
Av::Encoding
userTranscodeFormatToAvEncoding(AudioFormat format)
Av::Format
userTranscodeFormatToAvFormat(AudioFormat format)
{
switch (format)
{
case AudioFormat::MP3: return Av::Encoding::MP3;
case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
default: return Av::Encoding::OGG_OPUS;
case AudioFormat::MP3: return Av::Format::MP3;
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
default: return Av::Format::OGG_OPUS;
}
}
@@ -87,19 +87,19 @@ getStreamParameters(RequestContext& context)
throw UserNotAuthorizedError {};
// format = "raw" => no transcode. Other format values will be ignored
const bool transcode {(!format || (*format != "raw")) && user->getAudioTranscodeEnable()};
const bool transcode {(!format || (*format != "raw")) && user->getSubsonicTranscodeEnable()};
if (transcode)
{
// "If set to zero, no limit is imposed"
if (!maxBitRate || *maxBitRate == 0)
maxBitRate = user->getAudioTranscodeBitrate() / 1000;
std::size_t bitRate {user->getSubsonicTranscodeBitrate() / 1000};
*maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000);
// "If set to zero, no limit is imposed"
if (maxBitRate && *maxBitRate != 0)
bitRate = clamp(*maxBitRate, std::size_t {48}, bitRate);
Av::TranscodeParameters transcodeParameters;
transcodeParameters.bitrate = *maxBitRate * 1000;
transcodeParameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat());
transcodeParameters.bitrate = bitRate * 1000;
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
parameters.transcodeParameters = std::move(transcodeParameters);
+6 -17
View File
@@ -30,7 +30,6 @@
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/SubsonicSettings.hpp"
#include "database/Track.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackList.hpp"
@@ -579,7 +578,6 @@ handleCreateUserRequest(RequestContext& context)
throw UserAlreadyExistsGenericError {};
User::pointer user {User::create(context.dbSession, username, hash)};
user.modify()->setMaxAudioTranscodeBitrate(128000);
return Response::createOkResponse();
}
@@ -903,11 +901,11 @@ handleGetArtistsRequest(RequestContext& context)
throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType;
switch (SubsonicSettings::get(context.dbSession)->getArtistListMode())
switch (user->getSubsonicArtistListMode())
{
case SubsonicSettings::ArtistListMode::AllArtists:
case User::SubsonicArtistListMode::AllArtists:
break;
case SubsonicSettings::ArtistListMode::ReleaseArtists:
case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist;
break;
}
@@ -1046,11 +1044,11 @@ handleGetIndexesRequest(RequestContext& context)
throw UserNotAuthorizedError {};
std::optional<TrackArtistLink::Type> linkType;
switch (SubsonicSettings::get(context.dbSession)->getArtistListMode())
switch (user->getSubsonicArtistListMode())
{
case SubsonicSettings::ArtistListMode::AllArtists:
case User::SubsonicArtistListMode::AllArtists:
break;
case SubsonicSettings::ArtistListMode::ReleaseArtists:
case User::SubsonicArtistListMode::ReleaseArtists:
linkType = TrackArtistLink::Type::ReleaseArtist;
break;
}
@@ -1520,7 +1518,6 @@ handleUpdateUserRequest(RequestContext& context)
{
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
std::optional<std::string> password {getParameterAs<std::string>(context.parameters, "password")};
std::optional<Bitrate> maxBitRate {getParameterAs<Bitrate>(context.parameters, "maxBitRate")};
User::PasswordHash hash;
if (password)
@@ -1538,14 +1535,6 @@ handleUpdateUserRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
if (maxBitRate)
{
if (*maxBitRate == 0)
*maxBitRate = 320;
user.modify()->setMaxAudioTranscodeBitrate(*maxBitRate * 1000);
}
if (password)
{
user.modify()->setPasswordHash(hash);
-1
View File
@@ -13,7 +13,6 @@ add_executable(lms
ui/admin/DatabaseSettingsView.cpp
ui/admin/DatabaseStatus.cpp
ui/admin/InitWizardView.cpp
ui/admin/SubsonicView.cpp
ui/admin/UserView.cpp
ui/admin/UsersView.cpp
ui/common/Validators.cpp
+2 -8
View File
@@ -28,7 +28,6 @@
#include "av/AvTranscoder.hpp"
#include "cover/ICoverArtGrabber.hpp"
#include "database/Db.hpp"
#include "database/SubsonicSettings.hpp"
#include "scanner/IMediaScanner.hpp"
#include "recommendation/IEngine.hpp"
#include "subsonic/SubsonicResource.hpp"
@@ -170,13 +169,8 @@ int main(int argc, char* argv[])
API::Subsonic::SubsonicResource subsonicResource {database};
// bind API resources
{
Database::Session session {database};
auto transaction {session.createSharedTransaction()};
if (Database::SubsonicSettings::get(session)->isAPIEnabled())
server.addResource(&subsonicResource, subsonicResource.getPath());
}
if (ServiceProvider<IConfig>::get()->getBool("api-subsonic", true))
server.addResource(&subsonicResource, subsonicResource.getPath());
// bind UI entry point
server.addEntryPoint(Wt::EntryPointType::Application,
+10 -21
View File
@@ -43,7 +43,6 @@
#include "admin/DatabaseSettingsView.hpp"
#include "admin/UserView.hpp"
#include "admin/UsersView.hpp"
#include "admin/SubsonicView.hpp"
#include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodeResource.hpp"
#include "resource/ImageResource.hpp"
@@ -123,7 +122,6 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
// Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database");
messageResourceBundle().use(appRoot() + "admin-initwizard");
messageResourceBundle().use(appRoot() + "admin-subsonic");
messageResourceBundle().use(appRoot() + "admin-user");
messageResourceBundle().use(appRoot() + "admin-users");
messageResourceBundle().use(appRoot() + "artist");
@@ -344,7 +342,6 @@ enum IdxRoot
IdxAdminDatabase,
IdxAdminUsers,
IdxAdminUser,
IdxAdminSubsonic,
};
static void
@@ -368,7 +365,6 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
{ "/admin/database", IdxAdminDatabase, true },
{ "/admin/users", IdxAdminUsers, true },
{ "/admin/user", IdxAdminUser, true },
{ "/admin/subsonic", IdxAdminSubsonic, true },
};
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
@@ -445,6 +441,9 @@ LmsApplication::createHome()
navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"});
navbar->setResponsive(true);
// MediaPlayer
_mediaPlayer = main->bindNew<MediaPlayer>("player");
Wt::WMenu* menu {navbar->addMenu(std::make_unique<Wt::WMenu>())};
{
auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists"));
@@ -488,10 +487,6 @@ LmsApplication::createHome()
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
usersSettings->setSelectable(false);
auto subsonicSettings = admin->insertItem(2, Wt::WString::tr("Lms.Admin.Subsonic.subsonic"));
subsonicSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/subsonic"));
subsonicSettings->setSelectable(false);
menuItem->setMenu(std::move(admin));
}
@@ -522,7 +517,6 @@ LmsApplication::createHome()
mainStack->addNew<DatabaseSettingsView>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
mainStack->addNew<SubsonicView>();
}
explore->tracksAdd.connect([=] (const std::vector<Database::IdType>& trackIds)
@@ -536,33 +530,28 @@ LmsApplication::createHome()
});
// MediaPlayer
MediaPlayer* player = main->bindNew<MediaPlayer>("player");
// Events from MediaPlayer
player->playNext.connect([=]
_mediaPlayer->playNext.connect([=]
{
playqueue->playNext();
});
player->playPrevious.connect([=]
_mediaPlayer->playPrevious.connect([=]
{
playqueue->playPrevious();
});
player->playbackEnded.connect([=]
_mediaPlayer->playbackEnded.connect([=]
{
playqueue->playNext();
});
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play)
playqueue->trackSelected.connect([=] (Database::IdType trackId, bool play, float replayGain)
{
_events.lastLoadedTrackId = trackId;
_events.trackLoaded(trackId, play);
_mediaPlayer->loadTrack(trackId, play, replayGain);
});
playqueue->trackUnselected.connect([=]
playqueue->trackUnselected.connect([=] ()
{
_events.lastLoadedTrackId.reset();
_events.trackUnloaded();
_mediaPlayer->stop();
});
// Events from MediaScanner
+4 -6
View File
@@ -44,6 +44,7 @@ class AudioFileResource;
class Auth;
class ImageResource;
class LmsApplicationException;
class MediaPlayer;
// Events that can be listen from anywhere in the application
struct Events
@@ -52,12 +53,6 @@ struct Events
Wt::Signal<LmsApplicationInfo> appOpen;
Wt::Signal<LmsApplicationInfo> appClosed;
// A track is being loaded
Wt::Signal<Database::IdType /* trackId */, bool /* play */> trackLoaded;
std::optional<Database::IdType> lastLoadedTrackId;
// Unload current track
Wt::Signal<> trackUnloaded;
// Database events
Wt::Signal<> dbScanned;
Wt::Signal<Scanner::ScanProgressStats> dbScanInProgress;
@@ -105,6 +100,8 @@ class LmsApplication : public Wt::WApplication
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Wt::Dbo::ptr<Database::Release> release, bool addText = true);
static std::unique_ptr<Wt::WText> createCluster(Wt::Dbo::ptr<Database::Cluster> cluster, bool canDelete = false);
MediaPlayer* getMediaPlayer() const { return _mediaPlayer; }
// Signal emitted just before the session ends (user may already be logged out)
Wt::Signal<>& preQuit() { return _preQuit; }
@@ -133,6 +130,7 @@ class LmsApplication : public Wt::WApplication
std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource;
std::shared_ptr<AudioFileResource> _audioFileResource;
std::shared_ptr<ImageResource> _imageResource;
MediaPlayer* _mediaPlayer {};
};
+2 -2
View File
@@ -86,8 +86,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
case Database::User::UITheme::Dark:
return
{
{"css/bootstrap-darkly.min.css"},
{"resources/themes/bootstrap/3/wt.css"},
{"css/bootstrap-darkly.min.css"},
{"css/lms.css"},
{"css/lms-darkly.css"},
};
@@ -95,8 +95,8 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme)
case Database::User::UITheme::Light:
return
{
{"css/bootstrap-flatly.min.css"},
{"resources/themes/bootstrap/3/wt.css"},
{"css/bootstrap-flatly.min.css"},
{"css/lms.css"},
{"css/lms-flatly.css"},
};
+253 -71
View File
@@ -19,6 +19,10 @@
#include "MediaPlayer.hpp"
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "utils/Logger.hpp"
#include "database/Artist.hpp"
@@ -31,99 +35,263 @@
#include "resource/AudioFileResource.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
static std::string settingsToJSString(const MediaPlayer::Settings& settings)
{
namespace Json = Wt::Json;
Json::Object res;
{
Json::Object transcode;
transcode["mode"] = static_cast<int>(settings.transcode.mode);
transcode["format"] = static_cast<int>(settings.transcode.format);
transcode["bitrate"] = static_cast<int>(settings.transcode.bitrate);
res["transcode"] = std::move(transcode);
}
{
Json::Object replayGain;
replayGain["mode"] = static_cast<int>(settings.replayGain.mode);
replayGain["preAmpGain"] = settings.replayGain.preAmpGain;
replayGain["preAmpGainIfNoInfo"] = settings.replayGain.preAmpGainIfNoInfo;
res["replayGain"] = std::move(replayGain);
}
return Json::serialize(res);
}
static
std::optional<MediaPlayer::Settings::Transcode::Mode>
transcodeModeFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
MediaPlayer::Settings::Transcode::Mode mode {static_cast<MediaPlayer::Settings::Transcode::Mode>(*value)};
switch (mode)
{
case MediaPlayer::Settings::Transcode::Mode::Never:
case MediaPlayer::Settings::Transcode::Mode::Always:
case MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported:
return mode;
}
return std::nullopt;
}
static
std::optional<MediaPlayer::Format>
formatFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
MediaPlayer::Format format {static_cast<MediaPlayer::Format>(*value)};
switch (format)
{
case MediaPlayer::Format::MP3:
case MediaPlayer::Format::OGG_OPUS:
case MediaPlayer::Format::MATROSKA_OPUS:
case MediaPlayer::Format::OGG_VORBIS:
case MediaPlayer::Format::WEBM_VORBIS:
return format;
}
return std::nullopt;
}
static
std::optional<MediaPlayer::Bitrate>
bitrateFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates))
return *value;
return std::nullopt;
}
static
std::optional<MediaPlayer::Settings::ReplayGain::Mode>
replayGainModeFromString(const std::string& str)
{
const auto value {StringUtils::readAs<int>(str)};
if (!value)
return std::nullopt;
MediaPlayer::Settings::ReplayGain::Mode mode {static_cast<MediaPlayer::Settings::ReplayGain::Mode>(*value)};
switch (mode)
{
case MediaPlayer::Settings::ReplayGain::Mode::None:
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
case MediaPlayer::Settings::ReplayGain::Mode::Track:
case MediaPlayer::Settings::ReplayGain::Mode::Release:
return mode;
}
return std::nullopt;
}
static
std::optional<float>
replayGainPreAmpGainFromString(const std::string& str)
{
const auto value {StringUtils::readAs<double>(str)};
if (!value)
return std::nullopt;
return clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
}
static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings)
{
using Settings = MediaPlayer::Settings;
namespace Json = Wt::Json;
Json::Object parsedSettings;
Json::parse(strSettings, parsedSettings);
MediaPlayer::Settings settings;
{
const Json::Value transcodeValue {parsedSettings.get("transcode")};
if (transcodeValue.type() == Json::Type::Object)
{
const Json::Object transcode {transcodeValue};
settings.transcode.mode = transcodeModeFromString(transcode.get("mode").toString().orIfNull("")).value_or(Settings::Transcode::defaultMode);
settings.transcode.format = formatFromString(transcode.get("format").toString().orIfNull("")).value_or(Settings::Transcode::defaultFormat);
settings.transcode.bitrate = bitrateFromString(transcode.get("bitrate").toString().orIfNull("")).value_or(Settings::Transcode::defaultBitrate);
}
}
{
const Json::Value replayGainValue {parsedSettings.get("replayGain")};
if (replayGainValue.type() == Json::Type::Object)
{
const Json::Object replayGain {replayGainValue};
settings.replayGain.mode = replayGainModeFromString(replayGain.get("mode").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultMode);
settings.replayGain.preAmpGain = replayGainPreAmpGainFromString(replayGain.get("preAmpGain").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain);
settings.replayGain.preAmpGainIfNoInfo = replayGainPreAmpGainFromString(replayGain.get("preAmpGainIfNoInfo").toString().orIfNull("")).value_or(Settings::ReplayGain::defaultPreAmpGain);
}
}
return settings;
}
MediaPlayer::MediaPlayer()
: Wt::WTemplate(Wt::WString::tr("Lms.MediaPlayer.template")),
playbackEnded(this, "playbackEnded"),
playPrevious(this, "playPrevious"),
playNext(this, "playNext")
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")},
playbackEnded {this, "playbackEnded"},
playPrevious {this, "playPrevious"},
playNext {this, "playNext"},
_settingsLoaded {this, "settingsLoaded"}
{
_title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release");
wApp->doJavaScript("LMS.mediaplayer.init(" + jsRef() + ")");
_settingsLoaded.connect([this](const std::string& settings)
{
LMS_LOG(UI, DEBUG) << "Settings loaded! '" << settings << "'";
LmsApp->getEvents().trackLoaded.connect(this, &MediaPlayer::loadTrack);
LmsApp->getEvents().trackUnloaded.connect(this, &MediaPlayer::stop);
_settings = settingsfromJSString(settings);
settingsLoaded.emit();
});
{
Settings defaultSettings;
std::ostringstream oss;
oss << "LMS.mediaplayer.init("
<< jsRef()
<< ", defaultSettings = " << settingsToJSString(defaultSettings)
<< ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str());
}
}
void
MediaPlayer::loadTrack(Database::IdType trackId, bool play)
MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
{
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
std::string resource;
bool transcode;
if (LmsApp->getUser()->getAudioTranscodeEnable())
{
resource = LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser());
transcode = true;
}
else
{
resource = LmsApp->getAudioFileResource()->getUrl(trackId);
transcode = false;
}
const auto artists {track->getArtists()};
std::ostringstream oss;
oss
<< "var params = {"
<< " mode: " << (transcode ? "Mode.Transcode" : "Mode.File") << ","
<< " resource: \"" << resource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]"
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
if (!track)
return;
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
const auto artists {track->getArtists()};
oss
<< "var params = {"
<< " nativeResource: \"" << nativeResource << "\","
<< " transcodeResource: \"" << transcodeResource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " replayGain: " << replayGain << ","
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]"
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
}
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
wApp->doJavaScript(oss.str());
_trackIdLoaded = trackId;
trackLoaded.emit(*_trackIdLoaded);
}
void
@@ -132,5 +300,19 @@ MediaPlayer::stop()
wApp->doJavaScript("LMS.mediaplayer.stop()");
}
void
MediaPlayer::setSettings(const Settings& settings)
{
_settings = settings;
{
std::ostringstream oss;
oss << "LMS.mediaplayer.setSettings(settings = " << settingsToJSString(settings) << ")";
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
doJavaScript(oss.str());
}
}
} // namespace UserInterface
+73 -5
View File
@@ -25,23 +25,91 @@
#include <Wt/WText.h>
#include "database/Types.hpp"
#include "database/User.hpp"
namespace UserInterface {
class MediaPlayer : public Wt::WTemplate
{
public:
using Bitrate = Database::Bitrate;
using Format = Database::AudioFormat;
using Gain = float;
// Do not change enum values as they may be stored locally in browser
// Keep it sync with LMS.mediaplayer js
struct Settings
{
struct Transcode
{
enum class Mode
{
Never = 0,
Always = 1,
IfFormatNotSupported = 2,
};
static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported};
static inline constexpr Format defaultFormat {Format::OGG_OPUS};
static inline constexpr Bitrate defaultBitrate {128000};
Mode mode {defaultMode};
Format format {defaultFormat};
Bitrate bitrate {defaultBitrate};
};
struct ReplayGain
{
enum class Mode
{
None = 0,
Auto = 1,
Track = 2,
Release = 3,
};
static inline constexpr Mode defaultMode {Mode::None};
static inline constexpr Gain defaultPreAmpGain {};
static inline constexpr Gain minPreAmpGain {-15};
static inline constexpr Gain maxPreAmpGain {15};
Mode mode {defaultMode};
Gain preAmpGain {defaultPreAmpGain};
Gain preAmpGainIfNoInfo {defaultPreAmpGain};
};
Transcode transcode;
ReplayGain replayGain;
};
MediaPlayer();
MediaPlayer(const MediaPlayer&) = delete;
MediaPlayer(MediaPlayer&&) = delete;
MediaPlayer& operator=(const MediaPlayer&) = delete;
MediaPlayer& operator=(MediaPlayer&&) = delete;
std::optional<Database::IdType> getTrackLoaded() const { return _trackIdLoaded; }
void loadTrack(Database::IdType trackId, bool play, float replayGain);
void stop();
std::optional<Settings> getSettings() const { return _settings; }
void setSettings(const Settings& settings);
// Signals
Wt::JSignal<> playbackEnded;
Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext;
Wt::JSignal<> playbackEnded;
Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext;
Wt::Signal<Database::IdType> trackLoaded;
Wt::Signal<> settingsLoaded;
private:
void stop();
void loadTrack(Database::IdType trackId, bool play);
std::optional<Database::IdType> _trackIdLoaded;
std::optional<Settings> _settings;
Wt::JSignal<std::string> _settingsLoaded;
Wt::WText* _title;
Wt::WAnchor* _release;
Wt::WAnchor* _artist;
+2 -1
View File
@@ -30,6 +30,7 @@
#include "resource/ImageResource.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
namespace {
@@ -99,7 +100,7 @@ PlayHistory::PlayHistory()
addSome();
});
LmsApp->getEvents().trackLoaded.connect([=](Database::IdType trackId, bool /* play */)
LmsApp->getMediaPlayer()->trackLoaded.connect([=](Database::IdType trackId)
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
+67 -4
View File
@@ -33,8 +33,9 @@
#include "utils/String.hpp"
#include "resource/ImageResource.hpp"
#include "TrackStringUtils.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp"
namespace UserInterface {
@@ -135,8 +136,13 @@ PlayQueue::PlayQueue()
if (!LmsApp->getUser()->isDemo())
{
LmsApp->post([=]
LmsApp->getMediaPlayer()->settingsLoaded.connect([=]
{
if (_mediaPlayerSettingsLoaded)
return;
_mediaPlayerSettingsLoaded = true;
std::size_t trackPos {};
{
@@ -176,7 +182,7 @@ PlayQueue::updateRadioBtn()
}
Database::TrackList::pointer
PlayQueue::getTrackList()
PlayQueue::getTrackList() const
{
return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId);
}
@@ -209,6 +215,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
Database::IdType trackId {};
bool addRadioTrack {};
std::optional<float> replayGain {};
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
@@ -235,6 +242,8 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
trackId = track.id();
replayGain = getReplayGain(pos, track);
if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
}
@@ -244,7 +253,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
updateCurrentTrack(true);
trackSelected.emit(trackId, play);
trackSelected.emit(trackId, play, replayGain ? *replayGain : 0);
}
void
@@ -432,5 +441,59 @@ PlayQueue::enqueueRadioTrack()
enqueueTracks(trackToAddIds);
}
std::optional<float>
PlayQueue::getReplayGain(std::size_t pos, const Database::Track::pointer& track) const
{
const auto& settings {LmsApp->getMediaPlayer()->getSettings()};
if (!settings)
return std::nullopt;
std::optional<MediaPlayer::Gain> gain;
switch (settings->replayGain.mode)
{
case MediaPlayer::Settings::ReplayGain::Mode::None:
return std::nullopt;
case MediaPlayer::Settings::ReplayGain::Mode::Track:
gain = track->getTrackReplayGain();
break;
case MediaPlayer::Settings::ReplayGain::Mode::Release:
gain = track->getReleaseReplayGain();
if (!gain)
gain = track->getTrackReplayGain();
break;
case MediaPlayer::Settings::ReplayGain::Mode::Auto:
{
const auto trackList {getTrackList()};
const auto prevEntry {pos > 0 ? trackList->getEntry(pos - 1) : Database::TrackListEntry::pointer {}};
const auto nextEntry {trackList->getEntry(pos + 1)};
const Database::Track::pointer prevTrack {prevEntry ? prevEntry->getTrack() : Database::Track::pointer {}};
const Database::Track::pointer nextTrack {nextEntry ? nextEntry->getTrack() : Database::Track::pointer {}};
if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease())
||
(nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease()))
{
gain = track->getReleaseReplayGain();
if (!gain)
gain = track->getTrackReplayGain();
}
else
{
gain = track->getTrackReplayGain();
}
break;
}
}
if (gain)
return *gain + settings->replayGain.preAmpGain;
return settings->replayGain.preAmpGainIfNoInfo;
}
} // namespace UserInterface
+5 -2
View File
@@ -33,6 +33,7 @@ namespace Similarity {
}
namespace Database {
class Track;
class TrackList;
}
@@ -53,13 +54,13 @@ class PlayQueue : public Wt::WTemplate
void playPrevious();
// Signal emitted when a track is to be load(and optionally played)
Wt::Signal<Database::IdType /*trackId*/, bool /*play*/> trackSelected;
Wt::Signal<Database::IdType /*trackId*/, bool /*play*/, float /* replayGain */> trackSelected;
// Signal emitted when track is unselected (has to be stopped)
Wt::Signal<> trackUnselected;
private:
Wt::Dbo::ptr<Database::TrackList> getTrackList();
Wt::Dbo::ptr<Database::TrackList> getTrackList() const;
void clearTracks();
void enqueueTracks(const std::vector<Database::IdType>& trackIds);
@@ -76,9 +77,11 @@ class PlayQueue : public Wt::WTemplate
void addRadioTrackFromSimilarity(std::shared_ptr<Similarity::Finder> similarityFinder);
void addRadioTrackFromClusters();
std::optional<float> getReplayGain(std::size_t pos, const Wt::Dbo::ptr<Database::Track>& track) const;
bool _repeatAll {};
bool _radioMode {};
bool _mediaPlayerSettingsLoaded {};
Database::IdType _tracklistId {};
Wt::WContainerWidget* _entriesContainer {};
Wt::WPushButton* _showMore {};
+255 -65
View File
@@ -21,6 +21,8 @@
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WDoubleValidator.h>
#include <Wt/WDoubleSpinBox.h>
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
@@ -31,11 +33,13 @@
#include "common/ValueStringModel.hpp"
#include "auth/IPasswordService.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "LmsApplication.hpp"
#include "LmsTheme.hpp"
#include "MediaPlayer.hpp"
namespace UserInterface {
@@ -46,22 +50,38 @@ class SettingsModel : public Wt::WFormModel
public:
// Associate each field with a unique string literal.
static inline const Field DarkModeField {"dark-mode"};
static inline const Field TranscodeEnableField {"transcoding-enable"};
static inline const Field TranscodeFormatField {"transcoding-bitrate"};
static inline const Field TranscodeBitrateField {"transcoding-format"};
static inline const Field TranscodeModeField {"transcode-mode"};
static inline const Field TranscodeFormatField {"transcode-format"};
static inline const Field TranscodeBitrateField {"transcode-bitrate"};
static inline const Field ReplayGainModeField {"replaygain-mode"};
static inline const Field ReplayGainPreAmpGainField {"replaygain-preamp"};
static inline const Field ReplayGainPreAmpGainIfNoInfoField {"replaygain-preamp-no-rg-info"};
static inline const Field SubsonicArtistListModeField {"subsonic-artist-list-mode"};
static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"};
static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"};
static inline const Field SubsonicTranscodeBitrateField {"subsonic-transcode-bitrate"};
static inline const Field PasswordOldField {"password-old"};
static inline const Field PasswordField {"password"};
static inline const Field PasswordConfirmField {"password-confirm"};
using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
SettingsModel(bool withOldPassword)
: _withOldPassword {withOldPassword}
{
initializeModels();
addField(DarkModeField);
addField(TranscodeEnableField);
addField(TranscodeModeField);
addField(TranscodeBitrateField);
addField(TranscodeFormatField);
addField(ReplayGainModeField);
addField(ReplayGainPreAmpGainField);
addField(ReplayGainPreAmpGainIfNoInfoField);
addField(SubsonicTranscodeEnableField);
addField(SubsonicTranscodeBitrateField);
addField(SubsonicTranscodeFormatField);
if (_withOldPassword)
addField(PasswordOldField);
@@ -69,14 +89,31 @@ class SettingsModel : public Wt::WFormModel
addField(PasswordField);
addField(PasswordConfirmField);
setValidator(TranscodeModeField, createMandatoryValidator());
setValidator(TranscodeBitrateField, createMandatoryValidator());
setValidator(TranscodeFormatField, createMandatoryValidator());
setValidator(ReplayGainModeField, createMandatoryValidator());
auto createPreAmpValidator = []
{
auto preampGainValidator {std::make_unique<Wt::WDoubleValidator>()};
preampGainValidator->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
return preampGainValidator;
};
setValidator(ReplayGainPreAmpGainField, createPreAmpValidator());
setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator());
setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator());
setValidator(SubsonicTranscodeFormatField, createMandatoryValidator());
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> transcodeBitrateModel() { return _transcodeBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> transcodeFormatModel() { return _transcodeFormatModel; }
std::shared_ptr<TranscodeModeModel> getTranscodeModeModel() { return _transcodeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeBitrateModel() { return _transcodeBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodeFormatModel; }
std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; }
void saveData()
{
@@ -97,43 +134,103 @@ class SettingsModel : public Wt::WFormModel
user.modify()->setUITheme(newTheme);
}
user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField)));
{
MediaPlayer::Settings settings;
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
if (transcodeBitrateRow)
user.modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow));
auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))};
if (transcodeModeRow)
settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow);
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
if (transcodeFormatRow)
user.modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow));
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
if (transcodeFormatRow)
settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow);
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
if (transcodeBitrateRow)
settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow);
auto replayGainModeRow {_replayGainModeModel->getRowFromString(valueText(ReplayGainModeField))};
if (replayGainModeRow)
settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow);
settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField));
settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField));
LmsApp->getMediaPlayer()->setSettings(settings);
}
{
user.modify()->setSubsonicTranscodeEnable(Wt::asNumber(value(SubsonicTranscodeEnableField)));
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))};
if (subsonicTranscodeBitrateRow)
user.modify()->setSubsonicTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow));
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))};
if (subsonicTranscodeFormatRow)
user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
}
if (!valueText(PasswordField).empty())
{
user.modify()->setPasswordHash(passwordHash);
user.modify()->clearAuthTokens();
}
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
if (subsonicArtistListModeRow)
user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow));
}
void loadData()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
setValue(DarkModeField, LmsApp->getUser()->getUITheme() == User::UITheme::Dark);
User::pointer user {LmsApp->getUser()};
setValue(DarkModeField, user->getUITheme() == User::UITheme::Dark);
setValue(TranscodeEnableField, LmsApp->getUser()->getAudioTranscodeEnable());
if (!LmsApp->getUser()->getAudioTranscodeEnable())
{
setReadOnly(TranscodeFormatField, true);
setReadOnly(TranscodeBitrateField, true);
const auto& settings {*LmsApp->getMediaPlayer()->getSettings()};
auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)};
if (transcodeModeRow)
setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow));
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)};
if (transcodeFormatRow)
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow));
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)};
if (transcodeBitrateRow)
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow));
auto replayGainModeRow {_replayGainModeModel->getRowFromValue(settings.replayGain.mode)};
if (replayGainModeRow)
setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow));
setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain);
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
}
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(LmsApp->getUser()->getAudioTranscodeBitrate())};
if (transcodeBitrateRow)
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow));
setValue(SubsonicTranscodeEnableField, LmsApp->getUser()->getSubsonicTranscodeEnable());
if (!LmsApp->getUser()->getSubsonicTranscodeEnable())
{
setReadOnly(SubsonicTranscodeFormatField, true);
setReadOnly(SubsonicTranscodeBitrateField, true);
}
auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(LmsApp->getUser()->getAudioTranscodeFormat())};
if (transcodeFormatRow)
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow));
auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicTranscodeBitrate())};
if (subsonicTranscodeBitrateRow)
setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow));
auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicTranscodeFormat())};
if (subsonicTranscodeFormatRow)
setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow));
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode())};
if (subsonicArtistListModeRow)
setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow));
}
private:
@@ -207,41 +304,56 @@ class SettingsModel : public Wt::WFormModel
void initializeModels()
{
Bitrate maxAudioBitrate;
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
maxAudioBitrate = LmsApp->getUser()->getMaxAudioTranscodeBitrate();
}
_transcodeModeModel = std::make_shared<TranscodeModeModel>();
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported);
_transcodeBitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates)
{
if (bitrate > maxAudioBitrate)
break;
_transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
}
_transcodeFormatModel = std::make_shared<ValueStringModel<AudioFormat>>();
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.mp3"), AudioFormat::MP3);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_opus"), AudioFormat::OGG_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.matroska_opus"), AudioFormat::MATROSKA_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_vorbis"), AudioFormat::OGG_VORBIS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.webm_vorbis"), AudioFormat::WEBM_VORBIS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS);
_replayGainModeModel = std::make_shared<ReplayGainModeModel>();
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None);
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto);
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track);
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release);
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<User::SubsonicArtistListMode>>();
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists);
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists);
}
bool _withOldPassword {};
std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel;
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel;
std::shared_ptr<TranscodeModeModel> _transcodeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel;
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel;
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
std::shared_ptr<ValueStringModel<User::SubsonicArtistListMode>> _subsonicArtistListModeModel;
};
SettingsView::SettingsView()
{
wApp->internalPathChanged().connect(std::bind([=]
wApp->internalPathChanged().connect([=]
{
refreshView();
}));
});
LmsApp->getMediaPlayer()->settingsLoaded.connect([=]()
{
refreshView();
});
refreshView();
}
@@ -254,6 +366,10 @@ SettingsView::refreshView()
clear();
// Hack to wait for the audio player know the settings applied
if (!LmsApp->getMediaPlayer()->getSettings())
return;
auto t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template"))};
auto model {std::make_shared<SettingsModel>(!LmsApp->isUserAuthStrong())};
@@ -268,6 +384,7 @@ SettingsView::refreshView()
if (!LmsApp->isUserAuthStrong())
{
t->setCondition("if-has-old-password", true);
auto oldPassword {std::make_unique<Wt::WLineEdit>()};
oldPassword->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword));
@@ -283,35 +400,108 @@ SettingsView::refreshView()
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
// Transcoding
auto transcode {std::make_unique<Wt::WCheckBox>()};
auto* transcodeRaw {transcode.get()};
t->setFormWidget(SettingsModel::TranscodeEnableField, std::move(transcode));
// Format
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
transcodeFormat->setModel(model->transcodeFormatModel());
t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat));
// Bitrate
auto transcodeBitrate {std::make_unique<Wt::WComboBox>()};
transcodeBitrate->setModel(model->transcodeBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate));
transcodeRaw->changed().connect([=]()
// Audio
{
bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked};
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
// Transcode
auto transcodeMode {std::make_unique<Wt::WComboBox>()};
auto* transcodeModeRaw {transcodeMode.get()};
transcodeMode->setModel(model->getTranscodeModeModel());
t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode));
// Format
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
transcodeFormat->setModel(model->getTranscodeFormatModel());
t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat));
// Bitrate
auto transcodeBitrate {std::make_unique<Wt::WComboBox>()};
transcodeBitrate->setModel(model->getTranscodeBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate));
transcodeModeRaw->activated().connect([=](int row)
{
const bool enable {model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never};
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
if (LmsApp->getMediaPlayer()->getSettings()->transcode.mode == MediaPlayer::Settings::Transcode::Mode::Never)
{
model->setReadOnly(SettingsModel::TranscodeFormatField, true);
model->setReadOnly(SettingsModel::TranscodeBitrateField, true);
}
// Replay gain mode
auto replayGainMode {std::make_unique<Wt::WComboBox>()};
auto* replayGainModeRaw {replayGainMode.get()};
replayGainMode->setModel(model->getReplayGainModeModel());
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
// Replay gain preampGain
auto replayGainPreampGain {std::make_unique<Wt::WDoubleSpinBox>()};
replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain));
// Replay gain preampGain if no info
auto replayGainPreampGainIfNoInfo {std::make_unique<Wt::WDoubleSpinBox>()};
replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo));
replayGainModeRaw->activated().connect([=](int row)
{
const bool enable {model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None};
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable);
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
if (LmsApp->getMediaPlayer()->getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None)
{
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, true);
}
}
// Subsonic
{
t->setCondition("if-has-subsonic-api", ServiceProvider<IConfig>::get()->getBool("api-subsonic", true));
// Transcode
auto transcode {std::make_unique<Wt::WCheckBox>()};
auto* transcodeRaw {transcode.get()};
t->setFormWidget(SettingsModel::SubsonicTranscodeEnableField, std::move(transcode));
// Format
auto transcodeFormat {std::make_unique<Wt::WComboBox>()};
transcodeFormat->setModel(model->getTranscodeFormatModel());
t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat));
// Bitrate
auto transcodeBitrate {std::make_unique<Wt::WComboBox>()};
transcodeBitrate->setModel(model->getTranscodeBitrateModel());
t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate));
// Artist list mode
auto artistListMode = std::make_unique<Wt::WComboBox>();
artistListMode->setModel(model->getSubsonicArtistListModeModel());
t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode));
transcodeRaw->changed().connect([=]()
{
const bool enable {transcodeRaw->checkState() == Wt::CheckState::Checked};
model->setReadOnly(SettingsModel::SubsonicTranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::SubsonicTranscodeBitrateField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
}
// Buttons
Wt::WPushButton *saveBtn {t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")))};
Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")))};
saveBtn->clicked().connect(std::bind([=] ()
saveBtn->clicked().connect([=]()
{
{
@@ -334,7 +524,7 @@ SettingsView::refreshView()
// Udate the view: Delete any validation message in the view, etc.
t->updateView(model.get());
}));
});
discardBtn->clicked().connect(std::bind([=] ()
{
-180
View File
@@ -1,180 +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 "SubsonicView.hpp"
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplateFormView.h>
#include <Wt/WFormModel.h>
#include "database/SubsonicSettings.hpp"
#include "utils/Logger.hpp"
#include "common/ValueStringModel.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
using namespace Database;
class SubsonicModel : public Wt::WFormModel
{
public:
static inline const Field EnableField {"enable"};
static inline const Field ArtistListModeField {"artist-list-mode"};
SubsonicModel()
{
addField(EnableField);
addField(ArtistListModeField);
initializeModels();
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> getArtistListModeModel() { return _artistListModeModel; }
void saveData()
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
auto settings {SubsonicSettings::get(LmsApp->getDbSession())};
settings.modify()->setAPIEnabled(Wt::asNumber(value(EnableField)));
auto artistListModeRow {_artistListModeModel->getRowFromString(valueText(ArtistListModeField))};
if (artistListModeRow)
settings.modify()->setArtistListMode(_artistListModeModel->getValue(*artistListModeRow));
}
void loadData()
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto settings {SubsonicSettings::get(LmsApp->getDbSession())};
setValue(EnableField, settings->isAPIEnabled());
auto artistListModeRow {_artistListModeModel->getRowFromValue(settings->getArtistListMode())};
if (artistListModeRow)
setValue(ArtistListModeField, _artistListModeModel->getString(*artistListModeRow));
}
void initializeModels()
{
_artistListModeModel = std::make_shared<ValueStringModel<SubsonicSettings::ArtistListMode>>();
_artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.all-artists"), SubsonicSettings::ArtistListMode::AllArtists);
_artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.release-artists"), SubsonicSettings::ArtistListMode::ReleaseArtists);
}
private:
bool validateField(Field field) override
{
if (field == EnableField)
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
if (SubsonicSettings::get(LmsApp->getDbSession())->isAPIEnabled() != Wt::asNumber(value(EnableField)))
setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid, Wt::WString::tr("Lms.Admin.Subsonic.need-restart")));
else
setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid));
return true;
}
return Wt::WFormModel::validateField(field);
}
std::shared_ptr<ValueStringModel<SubsonicSettings::ArtistListMode>> _artistListModeModel;
};
SubsonicView::SubsonicView()
{
wApp->internalPathChanged().connect([this]()
{
refreshView();
});
refreshView();
}
void
SubsonicView::refreshView()
{
if (!wApp->internalPathMatches("/admin/subsonic"))
return;
clear();
Wt::WTemplateFormView* t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Subsonic.template"))};
auto model {std::make_shared<SubsonicModel>()};
// Enable
auto enable {std::make_unique<Wt::WCheckBox>()};
auto* enableRaw {enable.get()};
t->setFormWidget(SubsonicModel::EnableField, std::move(enable));
// Artis tlist mode
auto artistListMode = std::make_unique<Wt::WComboBox>();
artistListMode->setModel(model->getArtistListModeModel());
t->setFormWidget(SubsonicModel::ArtistListModeField, std::move(artistListMode));
enableRaw->changed().connect([=]()
{
bool enable {enableRaw->checkState() == Wt::CheckState::Checked};
model->setReadOnly(SubsonicModel::ArtistListModeField, !enable);
t->updateModel(model.get());
t->updateView(model.get());
});
Wt::WPushButton* saveBtn = t->bindNew<Wt::WPushButton>("apply-btn", Wt::WString::tr("Lms.apply"));
Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")))};
saveBtn->clicked().connect([=]()
{
t->updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
}
t->updateView(model.get());
});
discardBtn->clicked().connect([=]()
{
model->loadData();
model->validate();
t->updateView(model.get());
});
t->updateView(model.get());
}
} // namespace UserInterface
-37
View File
@@ -1,37 +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>
namespace UserInterface {
class SubsonicView : public Wt::WContainerWidget
{
public:
SubsonicView();
private:
void refreshView();
};
} // namespace UserInterface
+7 -45
View File
@@ -48,10 +48,9 @@ class UserModel : public Wt::WFormModel
{
public:
static const Field LoginField;
static const Field PasswordField;
static const Field AudioTranscodeBitrateLimitField;
static const Field DemoField;
static inline const Field LoginField {"login"};
static inline const Field PasswordField {"password"};
static inline const Field DemoField {"demo"};
UserModel(std::optional<Database::IdType> userId)
: _userId {userId}
@@ -63,20 +62,15 @@ class UserModel : public Wt::WFormModel
}
addField(PasswordField);
addField(AudioTranscodeBitrateLimitField);
addField(DemoField);
if (!_userId)
setValidator(PasswordField, createMandatoryValidator());
initializeModels();
// populate the model with initial data
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> bitrateModel() { return _bitrateModel; }
void saveData()
{
std::optional<Database::User::PasswordHash> passwordHash;
@@ -96,20 +90,12 @@ class UserModel : public Wt::WFormModel
user.modify()->setPasswordHash(*passwordHash);
user.modify()->clearAuthTokens();
}
auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))};
if (transcodeBitrateLimitRow)
user.modify()->setMaxAudioTranscodeBitrate(_bitrateModel->getValue(*transcodeBitrateLimitRow));
}
else
{
// Create user
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8(), *passwordHash)};
auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))};
if (transcodeBitrateLimitRow )
user.modify()->setMaxAudioTranscodeBitrate(_bitrateModel->getValue(*transcodeBitrateLimitRow));
if (Wt::asNumber(value(DemoField)))
user.modify()->setType(Database::User::Type::DEMO);
}
@@ -129,10 +115,6 @@ class UserModel : public Wt::WFormModel
throw UserNotFoundException {*_userId};
else if (user == LmsApp->getUser())
throw UserNotAllowedException {};
auto transcodeBitrateLimitRow {_bitrateModel->getRowFromValue(user->getMaxAudioTranscodeBitrate())};
if (transcodeBitrateLimitRow)
setValue(AudioTranscodeBitrateLimitField, _bitrateModel->getString(*transcodeBitrateLimitRow));
}
std::string getLoginName() const
@@ -194,30 +176,15 @@ class UserModel : public Wt::WFormModel
return false;
}
void initializeModels()
{
_bitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
for (auto bitrate : Database::User::audioTranscodeAllowedBitrates)
_bitrateModel->add( Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate );
}
std::shared_ptr<ValueStringModel<Bitrate>> _bitrateModel;
std::optional<Database::IdType> _userId;
};
const Wt::WFormModel::Field UserModel::LoginField = "login";
const Wt::WFormModel::Field UserModel::PasswordField = "password";
const Wt::WFormModel::Field UserModel::AudioTranscodeBitrateLimitField = "audio-transcode-bitrate-limit";
const Wt::WFormModel::Field UserModel::DemoField = "demo";
UserView::UserView()
{
wApp->internalPathChanged().connect(std::bind([=]
wApp->internalPathChanged().connect([this]()
{
refreshView();
}));
});
refreshView();
}
@@ -262,18 +229,13 @@ UserView::refreshView()
passwordEdit->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit));
// Transcode bitrate limit
auto bitrate = std::make_unique<Wt::WComboBox>();
bitrate->setModel(model->bitrateModel());
t->setFormWidget(UserModel::AudioTranscodeBitrateLimitField, std::move(bitrate));
// Demo account
t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>());
if (!userId && ServiceProvider<IConfig>::get()->getBool("demo", false))
t->setCondition("if-demo", true);
Wt::WPushButton* saveBtn = t->bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create"));
saveBtn->clicked().connect(std::bind([=]
saveBtn->clicked().connect([=]()
{
t->updateModel(model.get());
@@ -287,7 +249,7 @@ UserView::refreshView()
{
t->updateView(model.get());
}
}));
});
t->updateView(model.get());
}
+2 -1
View File
@@ -26,6 +26,7 @@
#include "database/User.hpp"
#include "ArtistLink.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database;
@@ -44,7 +45,7 @@ ArtistsInfo::ArtistsInfo()
refreshRecentlyAdded();
});
LmsApp->getEvents().trackLoaded.connect([=]
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{
refreshMostPlayed();
});
+11 -9
View File
@@ -37,6 +37,7 @@
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp"
using namespace Database;
@@ -46,10 +47,10 @@ namespace UserInterface {
Release::Release(Filters* filters)
: _filters(filters)
{
wApp->internalPathChanged().connect(std::bind([=]
wApp->internalPathChanged().connect([=]()
{
refresh();
}));
});
refresh();
@@ -217,27 +218,28 @@ Release::refresh()
}
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([=]
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(std::bind([=]
addBtn->clicked().connect([=]()
{
tracksAdd.emit({trackId});
}));
});
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
{
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{
entry->bindString("is-playing", "Lms-entry-playing");
if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
}
}
}
+2 -1
View File
@@ -27,6 +27,7 @@
#include "resource/ImageResource.hpp"
#include "ReleaseLink.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database;
@@ -45,7 +46,7 @@ ReleasesInfo::ReleasesInfo()
refreshRecentlyAdded();
});
LmsApp->getEvents().trackLoaded.connect([=]
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{
refreshMostPlayed();
});
+3 -2
View File
@@ -27,6 +27,7 @@
#include "database/User.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
using namespace Database;
@@ -49,7 +50,7 @@ void addEntries(Wt::WContainerWidget *container, const std::vector<Track::pointe
namespace UserInterface {
TracksInfo::TracksInfo()
: Wt::WTemplate(Wt::WString::tr("Lms.Explore.TracksInfo.template"))
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.TracksInfo.template")}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
@@ -61,7 +62,7 @@ TracksInfo::TracksInfo()
refreshRecentlyAdded();
});
LmsApp->getEvents().trackLoaded.connect([=]
LmsApp->getMediaPlayer()->trackLoaded.connect([=]
{
refreshMostPlayed();
});
+5 -3
View File
@@ -34,6 +34,7 @@
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "TrackStringUtils.hpp"
using namespace Database;
@@ -174,14 +175,15 @@ Tracks::addSome()
}));
LmsApp->getEvents().trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId, bool /*play*/)
LmsApp->getMediaPlayer()->trackLoaded.connect(entry, [=] (Database::IdType loadedTrackId)
{
entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : "");
});
if (LmsApp->getEvents().lastLoadedTrackId && *LmsApp->getEvents().lastLoadedTrackId == trackId)
if (auto trackIdLoaded {LmsApp->getMediaPlayer()->getTrackLoaded()})
{
entry->bindString("is-playing", "Lms-entry-playing");
if (*trackIdLoaded == trackId)
entry->bindString("is-playing", "Lms-entry-playing");
}
}
+14 -19
View File
@@ -73,32 +73,27 @@ AudioTranscodeResource:: ~AudioTranscodeResource()
}
static
std::optional<Av::Encoding>
AudioFormatToAvEncoding(Database::AudioFormat format)
std::optional<Av::Format>
AudioFormatToAvFormat(Database::AudioFormat format)
{
switch (format)
{
case Database::AudioFormat::MP3: return Av::Encoding::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
case Database::AudioFormat::MP3: return Av::Format::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
}
LOG(ERROR) << "Cannot convert from audio format to encoding";
LOG(ERROR) << "Cannot convert from audio format to AV format";
return std::nullopt;
}
std::string
AudioTranscodeResource::getUrlForUser(Database::IdType trackId, Database::User::pointer user) const
AudioTranscodeResource::getUrl(Database::IdType trackId) const
{
std::string computedUrl {url() + "&trackid=" + std::to_string(trackId)};
computedUrl += "&format=" + std::to_string(static_cast<std::size_t>(user->getAudioTranscodeFormat()));
computedUrl += "&bitrate=" + std::to_string(LmsApp->getUser()->getAudioTranscodeBitrate());
return computedUrl;
return url() + "&trackid=" + std::to_string(trackId);
}
template<typename T>
@@ -144,8 +139,8 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
if (!trackId || !format || !bitrate)
return;
auto encoding {AudioFormatToAvEncoding(*format)};
if (!encoding)
auto avFormat {AudioFormatToAvFormat(*format)};
if (!avFormat)
return;
// optional parameter
@@ -166,7 +161,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
trackPath = track->getPath();
if (!LmsApp->getUser()->checkBitrate(*bitrate))
if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates))
{
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
return;
@@ -175,7 +170,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
Av::TranscodeParameters parameters {};
parameters.stripMetadata = true;
parameters.encoding = *encoding;
parameters.format = *avFormat;
parameters.bitrate = *bitrate;
parameters.offset = std::chrono::seconds {offset ? *offset : 0};
@@ -37,7 +37,7 @@ class AudioTranscodeResource : public Wt::WResource
~AudioTranscodeResource();
// Url depends on the user since settings are used in parameters
std::string getUrlForUser(Database::IdType trackId, Wt::Dbo::ptr<Database::User> user) const;
std::string getUrl(Database::IdType trackId) const;
void handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response);
+8 -1
View File
@@ -154,12 +154,19 @@ static
void
testSingleTrack(Session& session)
{
{
auto transaction {session.createSharedTransaction()};
CHECK(Track::getCount(session) == 0);
}
ScopedTrack track {session, "MyTrackFile"};
{
auto transaction {session.createUniqueTransaction()};
auto transaction {session.createSharedTransaction()};
CHECK(Track::getAll(session).size() == 1);
CHECK(Track::getCount(session) == 1);
}
}
+6
View File
@@ -124,6 +124,12 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const auto& audioStream : track->audioStreams)
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
if (track->trackReplayGain)
std::cout << "Track replay gain: " << *track->trackReplayGain << std::endl;
if (track->albumReplayGain)
std::cout << "Album replay gain: " << *track->albumReplayGain << std::endl;
if (track->acoustID)
std::cout << "AcoustID: " << track->acoustID->getAsString() << std::endl;