Merge branch 'develop' for release v3.12.0
This commit is contained in:
+61
-73
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {};
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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([=] ()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user