Merge branch 'develop' for release v3.14.0
This commit is contained in:
@@ -15,12 +15,13 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
|
|||||||
* User management
|
* User management
|
||||||
* Recommendation engine
|
* Recommendation engine
|
||||||
* Audio transcode for maximum interoperability and low bandwith requirements
|
* Audio transcode for maximum interoperability and low bandwith requirements
|
||||||
|
* ReplayGain support
|
||||||
* Persistent play queue across sessions
|
* Persistent play queue across sessions
|
||||||
* Compilation support
|
* Compilation support
|
||||||
* Multi-value tags: artists, genres, ...
|
* Multi-value tags: artists, genres, ...
|
||||||
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
||||||
* MusicBrainzID support to handle duplicated artist and release names
|
* MusicBrainzID support to handle duplicated artist and release names
|
||||||
* ReplayGain support
|
* Disc subtitles support
|
||||||
* _Systemd_ integration
|
* _Systemd_ integration
|
||||||
* Subsonic API, with the following additional features:
|
* Subsonic API, with the following additional features:
|
||||||
* Playlists
|
* Playlists
|
||||||
|
|||||||
+3
-1
@@ -26,7 +26,9 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle">
|
<div class="media-left media-middle">
|
||||||
${cover class="media-object Lms-cover"}
|
<div class="media-object Lms-cover-container-medium">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4 class="media-heading">
|
<h4 class="media-heading">
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden-xs col-sm-2 Lms-player-duration text-muted">
|
<div class="hidden-xs col-sm-2 Lms-player-duration text-muted">
|
||||||
|
<a id="lms-transcoding-active" class="Lms-player-btn Lms-control-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="display:none"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></a>
|
||||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,6 @@
|
|||||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
|
||||||
|
|
||||||
<!--User-->
|
<!--User-->
|
||||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
|
|
||||||
<message id="Lms.Admin.User.demo-account">Demo account</message>
|
<message id="Lms.Admin.User.demo-account">Demo account</message>
|
||||||
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
|
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
|
||||||
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
||||||
@@ -135,6 +134,9 @@
|
|||||||
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Search...</message>
|
||||||
<message id="Lms.Explore.Search.search">Search results</message>
|
<message id="Lms.Explore.Search.search">Search results</message>
|
||||||
|
|
||||||
|
<!--Player-->
|
||||||
|
<message id="Lms.Player.transcoding-active">Transcoding active</message>
|
||||||
|
|
||||||
<!--Playqueue-->
|
<!--Playqueue-->
|
||||||
<message id="Lms.PlayQueue.clear">Clear</message>
|
<message id="Lms.PlayQueue.clear">Clear</message>
|
||||||
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
|
||||||
|
|||||||
@@ -82,7 +82,6 @@
|
|||||||
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
|
||||||
|
|
||||||
<!--User-->
|
<!--User-->
|
||||||
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
|
|
||||||
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
|
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
|
||||||
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
|
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
|
||||||
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
|
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
|
||||||
@@ -135,6 +134,9 @@
|
|||||||
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
<message id="Lms.Explore.Search.search-placeholder">Rechercher...</message>
|
||||||
<message id="Lms.Explore.Search.search">Résultats de la recherche</message>
|
<message id="Lms.Explore.Search.search">Résultats de la recherche</message>
|
||||||
|
|
||||||
|
<!--Player-->
|
||||||
|
<message id="Lms.Player.transcoding-active">Transcodage actif</message>
|
||||||
|
|
||||||
<!--Playqueue-->
|
<!--Playqueue-->
|
||||||
<message id="Lms.PlayQueue.clear">Effacer</message>
|
<message id="Lms.PlayQueue.clear">Effacer</message>
|
||||||
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
|
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
<div class="col-xs-9 Lms-playqueue-entry-name">
|
<div class="col-xs-9 Lms-playqueue-entry-name">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle hidden-xs">
|
<div class="media-left media-middle hidden-xs">
|
||||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
<div class="media-object Lms-cover-container-small">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle">
|
<div class="media-left media-middle">
|
||||||
${cover class="media-object Lms-cover Lms-cover-container-medium"}
|
<div class="media-object Lms-cover-container-medium">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4 class="media-heading">
|
<h4 class="media-heading">
|
||||||
@@ -40,7 +42,9 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle">
|
<div class="media-left media-middle">
|
||||||
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
|
<div class="media-object Lms-cover-container-smaller">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<div class="media-heading">
|
<div class="media-heading">
|
||||||
|
|||||||
+3
-1
@@ -25,7 +25,9 @@
|
|||||||
<div class="col-xs-12 Lms-explore-tracks-entry-name">
|
<div class="col-xs-12 Lms-explore-tracks-entry-name">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle hidden-xs">
|
<div class="media-left media-middle hidden-xs">
|
||||||
${cover class="media-object Lms-cover"}
|
<div class="media-object Lms-cover-container-small">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||||
|
|||||||
+3
-1
@@ -26,7 +26,9 @@
|
|||||||
<div class="col-xs-9 Lms-explore-tracks-entry-name">
|
<div class="col-xs-9 Lms-explore-tracks-entry-name">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left media-middle hidden-xs">
|
<div class="media-left media-middle hidden-xs">
|
||||||
${cover class="media-object Lms-cover"}
|
<div class="media-object Lms-cover-container-small">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body media-middle">
|
<div class="media-body media-middle">
|
||||||
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
|
||||||
<!--FORMS message blocks-->
|
|
||||||
|
|
||||||
<message id="Lms.Explore.TracksInfo.template">
|
|
||||||
<h3>${tr:Lms.Explore.TracksInfo.most-played}</h3>
|
|
||||||
${most-played}
|
|
||||||
<h3>${tr:Lms.Explore.TracksInfo.recently-added}</h3>
|
|
||||||
${recently-added}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.Explore.TracksInfo.template.entry">
|
|
||||||
<div class="row Lms-explore-tracksinfo-entry Lms-vertical-align">
|
|
||||||
<div class="col-xs-12 Lms-trackname">
|
|
||||||
${name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
+2
-1
@@ -177,7 +177,8 @@ RUN \
|
|||||||
|
|
||||||
## Release Stage
|
## Release Stage
|
||||||
FROM alpine:3.10 AS release
|
FROM alpine:3.10 AS release
|
||||||
MAINTAINER Emeric Poupon <itmfr@yahoo.fr>
|
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
|
||||||
|
LABEL version=${LMS_VERSION}
|
||||||
|
|
||||||
ARG RUNTIME_PACKAGES=" \
|
ARG RUNTIME_PACKAGES=" \
|
||||||
openssl \
|
openssl \
|
||||||
|
|||||||
+25
-32
@@ -62,35 +62,41 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Lms-cover {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.Lms-cover:hover {
|
.Lms-cover:hover {
|
||||||
filter: brightness(130%);
|
filter: brightness(130%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-cover-container-smaller {
|
.Lms-cover-container-smaller {
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-cover-smaller {
|
|
||||||
width: 64px;
|
width: 64px;
|
||||||
border-radius: 3px;
|
height: 64px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-cover-container-small {
|
.Lms-cover-container-small {
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-cover-small {
|
|
||||||
width: 96px;
|
width: 96px;
|
||||||
border-radius: 3px;
|
height: 96px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-cover-container-medium {
|
.Lms-cover-container-medium {
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-cover-medium {
|
|
||||||
width: 128px;
|
width: 128px;
|
||||||
border-radius: 3px;
|
height: 128px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-cover-large {
|
.Lms-cover-large {
|
||||||
@@ -119,8 +125,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-releases-entry {
|
.Lms-explore-releases-entry {
|
||||||
padding-top: 8px;
|
padding-top: 4px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-release-cover {
|
.Lms-explore-release-cover {
|
||||||
@@ -181,8 +187,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-artist-entry {
|
.Lms-explore-artist-entry {
|
||||||
padding-top: 8px;
|
padding-top: 4px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-artist-entries-container {
|
.Lms-explore-artist-entries-container {
|
||||||
@@ -193,10 +199,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-tracks-entry {
|
|
||||||
min-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
|
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -221,11 +223,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-explore-tracksinfo-entry {
|
|
||||||
min-height: 28px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-header {
|
.Lms-header {
|
||||||
padding-bottom: 9px;
|
padding-bottom: 9px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -248,10 +245,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Lms-playqueue-entry {
|
|
||||||
min-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
|
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
File diff suppressed because one or more lines are too long
@@ -1,211 +0,0 @@
|
|||||||
/* ========================================================================
|
|
||||||
* Bootstrap: collapse.js v3.3.6
|
|
||||||
* http://getbootstrap.com/javascript/#collapse
|
|
||||||
* ========================================================================
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
* ======================================================================== */
|
|
||||||
|
|
||||||
|
|
||||||
+function ($) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// COLLAPSE PUBLIC CLASS DEFINITION
|
|
||||||
// ================================
|
|
||||||
|
|
||||||
var Collapse = function (element, options) {
|
|
||||||
this.$element = $(element)
|
|
||||||
this.options = $.extend({}, Collapse.DEFAULTS, options)
|
|
||||||
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
|
|
||||||
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
|
|
||||||
this.transitioning = null
|
|
||||||
|
|
||||||
if (this.options.parent) {
|
|
||||||
this.$parent = this.getParent()
|
|
||||||
} else {
|
|
||||||
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.toggle) this.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.VERSION = '3.3.6'
|
|
||||||
|
|
||||||
Collapse.TRANSITION_DURATION = 350
|
|
||||||
|
|
||||||
Collapse.DEFAULTS = {
|
|
||||||
toggle: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.dimension = function () {
|
|
||||||
var hasWidth = this.$element.hasClass('width')
|
|
||||||
return hasWidth ? 'width' : 'height'
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.show = function () {
|
|
||||||
if (this.transitioning || this.$element.hasClass('in')) return
|
|
||||||
|
|
||||||
var activesData
|
|
||||||
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
|
|
||||||
|
|
||||||
if (actives && actives.length) {
|
|
||||||
activesData = actives.data('bs.collapse')
|
|
||||||
if (activesData && activesData.transitioning) return
|
|
||||||
}
|
|
||||||
|
|
||||||
var startEvent = $.Event('show.bs.collapse')
|
|
||||||
this.$element.trigger(startEvent)
|
|
||||||
if (startEvent.isDefaultPrevented()) return
|
|
||||||
|
|
||||||
if (actives && actives.length) {
|
|
||||||
Plugin.call(actives, 'hide')
|
|
||||||
activesData || actives.data('bs.collapse', null)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dimension = this.dimension()
|
|
||||||
|
|
||||||
this.$element
|
|
||||||
.removeClass('collapse')
|
|
||||||
.addClass('collapsing')[dimension](0)
|
|
||||||
.attr('aria-expanded', true)
|
|
||||||
|
|
||||||
this.$trigger
|
|
||||||
.removeClass('collapsed')
|
|
||||||
.attr('aria-expanded', true)
|
|
||||||
|
|
||||||
this.transitioning = 1
|
|
||||||
|
|
||||||
var complete = function () {
|
|
||||||
this.$element
|
|
||||||
.removeClass('collapsing')
|
|
||||||
.addClass('collapse in')[dimension]('')
|
|
||||||
this.transitioning = 0
|
|
||||||
this.$element
|
|
||||||
.trigger('shown.bs.collapse')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$.support.transition) return complete.call(this)
|
|
||||||
|
|
||||||
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
|
|
||||||
|
|
||||||
this.$element
|
|
||||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
|
||||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.hide = function () {
|
|
||||||
if (this.transitioning || !this.$element.hasClass('in')) return
|
|
||||||
|
|
||||||
var startEvent = $.Event('hide.bs.collapse')
|
|
||||||
this.$element.trigger(startEvent)
|
|
||||||
if (startEvent.isDefaultPrevented()) return
|
|
||||||
|
|
||||||
var dimension = this.dimension()
|
|
||||||
|
|
||||||
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
|
|
||||||
|
|
||||||
this.$element
|
|
||||||
.addClass('collapsing')
|
|
||||||
.removeClass('collapse in')
|
|
||||||
.attr('aria-expanded', false)
|
|
||||||
|
|
||||||
this.$trigger
|
|
||||||
.addClass('collapsed')
|
|
||||||
.attr('aria-expanded', false)
|
|
||||||
|
|
||||||
this.transitioning = 1
|
|
||||||
|
|
||||||
var complete = function () {
|
|
||||||
this.transitioning = 0
|
|
||||||
this.$element
|
|
||||||
.removeClass('collapsing')
|
|
||||||
.addClass('collapse')
|
|
||||||
.trigger('hidden.bs.collapse')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$.support.transition) return complete.call(this)
|
|
||||||
|
|
||||||
this.$element
|
|
||||||
[dimension](0)
|
|
||||||
.one('bsTransitionEnd', $.proxy(complete, this))
|
|
||||||
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.toggle = function () {
|
|
||||||
this[this.$element.hasClass('in') ? 'hide' : 'show']()
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.getParent = function () {
|
|
||||||
return $(this.options.parent)
|
|
||||||
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
|
|
||||||
.each($.proxy(function (i, element) {
|
|
||||||
var $element = $(element)
|
|
||||||
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
|
|
||||||
}, this))
|
|
||||||
.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
|
|
||||||
var isOpen = $element.hasClass('in')
|
|
||||||
|
|
||||||
$element.attr('aria-expanded', isOpen)
|
|
||||||
$trigger
|
|
||||||
.toggleClass('collapsed', !isOpen)
|
|
||||||
.attr('aria-expanded', isOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTargetFromTrigger($trigger) {
|
|
||||||
var href
|
|
||||||
var target = $trigger.attr('data-target')
|
|
||||||
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
|
|
||||||
|
|
||||||
return $(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// COLLAPSE PLUGIN DEFINITION
|
|
||||||
// ==========================
|
|
||||||
|
|
||||||
function Plugin(option) {
|
|
||||||
return this.each(function () {
|
|
||||||
var $this = $(this)
|
|
||||||
var data = $this.data('bs.collapse')
|
|
||||||
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
|
||||||
|
|
||||||
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
|
|
||||||
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
|
|
||||||
if (typeof option == 'string') data[option]()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var old = $.fn.collapse
|
|
||||||
|
|
||||||
$.fn.collapse = Plugin
|
|
||||||
$.fn.collapse.Constructor = Collapse
|
|
||||||
|
|
||||||
|
|
||||||
// COLLAPSE NO CONFLICT
|
|
||||||
// ====================
|
|
||||||
|
|
||||||
$.fn.collapse.noConflict = function () {
|
|
||||||
$.fn.collapse = old
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// COLLAPSE DATA-API
|
|
||||||
// =================
|
|
||||||
|
|
||||||
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
|
|
||||||
var $this = $(this)
|
|
||||||
|
|
||||||
if (!$this.attr('data-target')) e.preventDefault()
|
|
||||||
|
|
||||||
var $target = getTargetFromTrigger($this)
|
|
||||||
var data = $target.data('bs.collapse')
|
|
||||||
var option = data ? 'toggle' : $this.data()
|
|
||||||
|
|
||||||
Plugin.call($target, option)
|
|
||||||
})
|
|
||||||
|
|
||||||
}(jQuery);
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/* ========================================================================
|
|
||||||
* Bootstrap: dropdown.js v3.3.6
|
|
||||||
* http://getbootstrap.com/javascript/#dropdowns
|
|
||||||
* ========================================================================
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
* ======================================================================== */
|
|
||||||
|
|
||||||
|
|
||||||
+function ($) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// DROPDOWN CLASS DEFINITION
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
var backdrop = '.dropdown-backdrop'
|
|
||||||
var toggle = '[data-toggle="dropdown"]'
|
|
||||||
var Dropdown = function (element) {
|
|
||||||
$(element).on('click.bs.dropdown', this.toggle)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dropdown.VERSION = '3.3.6'
|
|
||||||
|
|
||||||
function getParent($this) {
|
|
||||||
var selector = $this.attr('data-target')
|
|
||||||
|
|
||||||
if (!selector) {
|
|
||||||
selector = $this.attr('href')
|
|
||||||
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
|
|
||||||
}
|
|
||||||
|
|
||||||
var $parent = selector && $(selector)
|
|
||||||
|
|
||||||
return $parent && $parent.length ? $parent : $this.parent()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMenus(e) {
|
|
||||||
if (e && e.which === 3) return
|
|
||||||
$(backdrop).remove()
|
|
||||||
$(toggle).each(function () {
|
|
||||||
var $this = $(this)
|
|
||||||
var $parent = getParent($this)
|
|
||||||
var relatedTarget = { relatedTarget: this }
|
|
||||||
|
|
||||||
if (!$parent.hasClass('open')) return
|
|
||||||
|
|
||||||
if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return
|
|
||||||
|
|
||||||
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget))
|
|
||||||
|
|
||||||
if (e.isDefaultPrevented()) return
|
|
||||||
|
|
||||||
$this.attr('aria-expanded', 'false')
|
|
||||||
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Dropdown.prototype.toggle = function (e) {
|
|
||||||
var $this = $(this)
|
|
||||||
|
|
||||||
if ($this.is('.disabled, :disabled')) return
|
|
||||||
|
|
||||||
var $parent = getParent($this)
|
|
||||||
var isActive = $parent.hasClass('open')
|
|
||||||
|
|
||||||
clearMenus()
|
|
||||||
|
|
||||||
if (!isActive) {
|
|
||||||
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
|
|
||||||
// if mobile we use a backdrop because click events don't delegate
|
|
||||||
$(document.createElement('div'))
|
|
||||||
.addClass('dropdown-backdrop')
|
|
||||||
.insertAfter($(this))
|
|
||||||
.on('click', clearMenus)
|
|
||||||
}
|
|
||||||
|
|
||||||
var relatedTarget = { relatedTarget: this }
|
|
||||||
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget))
|
|
||||||
|
|
||||||
if (e.isDefaultPrevented()) return
|
|
||||||
|
|
||||||
$this
|
|
||||||
.trigger('focus')
|
|
||||||
.attr('aria-expanded', 'true')
|
|
||||||
|
|
||||||
$parent
|
|
||||||
.toggleClass('open')
|
|
||||||
.trigger($.Event('shown.bs.dropdown', relatedTarget))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
Dropdown.prototype.keydown = function (e) {
|
|
||||||
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return
|
|
||||||
|
|
||||||
var $this = $(this)
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
if ($this.is('.disabled, :disabled')) return
|
|
||||||
|
|
||||||
var $parent = getParent($this)
|
|
||||||
var isActive = $parent.hasClass('open')
|
|
||||||
|
|
||||||
if (!isActive && e.which != 27 || isActive && e.which == 27) {
|
|
||||||
if (e.which == 27) $parent.find(toggle).trigger('focus')
|
|
||||||
return $this.trigger('click')
|
|
||||||
}
|
|
||||||
|
|
||||||
var desc = ' li:not(.disabled):visible a'
|
|
||||||
var $items = $parent.find('.dropdown-menu' + desc)
|
|
||||||
|
|
||||||
if (!$items.length) return
|
|
||||||
|
|
||||||
var index = $items.index(e.target)
|
|
||||||
|
|
||||||
if (e.which == 38 && index > 0) index-- // up
|
|
||||||
if (e.which == 40 && index < $items.length - 1) index++ // down
|
|
||||||
if (!~index) index = 0
|
|
||||||
|
|
||||||
$items.eq(index).trigger('focus')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// DROPDOWN PLUGIN DEFINITION
|
|
||||||
// ==========================
|
|
||||||
|
|
||||||
function Plugin(option) {
|
|
||||||
return this.each(function () {
|
|
||||||
var $this = $(this)
|
|
||||||
var data = $this.data('bs.dropdown')
|
|
||||||
|
|
||||||
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
|
|
||||||
if (typeof option == 'string') data[option].call($this)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var old = $.fn.dropdown
|
|
||||||
|
|
||||||
$.fn.dropdown = Plugin
|
|
||||||
$.fn.dropdown.Constructor = Dropdown
|
|
||||||
|
|
||||||
|
|
||||||
// DROPDOWN NO CONFLICT
|
|
||||||
// ====================
|
|
||||||
|
|
||||||
$.fn.dropdown.noConflict = function () {
|
|
||||||
$.fn.dropdown = old
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// APPLY TO STANDARD DROPDOWN ELEMENTS
|
|
||||||
// ===================================
|
|
||||||
|
|
||||||
$(document)
|
|
||||||
.on('click.bs.dropdown.data-api', clearMenus)
|
|
||||||
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
|
|
||||||
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
|
|
||||||
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
|
|
||||||
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown)
|
|
||||||
|
|
||||||
}(jQuery);
|
|
||||||
@@ -137,6 +137,9 @@ LMS.mediaplayer = function () {
|
|||||||
_elems.duration = document.getElementById("lms-mp-duration");
|
_elems.duration = document.getElementById("lms-mp-duration");
|
||||||
_elems.volume = document.getElementById("lms-mp-volume");
|
_elems.volume = document.getElementById("lms-mp-volume");
|
||||||
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
|
||||||
|
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
|
||||||
|
|
||||||
|
$(_elems.transcodingActive).tooltip()
|
||||||
|
|
||||||
var source = _audioCtx.createMediaElementSource(_elems.audio);
|
var source = _audioCtx.createMediaElementSource(_elems.audio);
|
||||||
source.connect(_gainNode);
|
source.connect(_gainNode);
|
||||||
@@ -198,6 +201,15 @@ LMS.mediaplayer = function () {
|
|||||||
Wt.emit(_root, "playbackEnded");
|
Wt.emit(_root, "playbackEnded");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_elems.audio.addEventListener("canplay", function() {
|
||||||
|
if (_getAudioMode() == Mode.Transcode) {
|
||||||
|
_elems.transcodingActive.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_elems.transcodingActive.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_initVolume();
|
_initVolume();
|
||||||
_initDefaultSettings(defaultSettings);
|
_initDefaultSettings(defaultSettings);
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/* ========================================================================
|
|
||||||
* Bootstrap: transition.js v3.3.6
|
|
||||||
* http://getbootstrap.com/javascript/#transitions
|
|
||||||
* ========================================================================
|
|
||||||
* Copyright 2011-2015 Twitter, Inc.
|
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
|
||||||
* ======================================================================== */
|
|
||||||
|
|
||||||
|
|
||||||
+function ($) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function transitionEnd() {
|
|
||||||
var el = document.createElement('bootstrap')
|
|
||||||
|
|
||||||
var transEndEventNames = {
|
|
||||||
WebkitTransition : 'webkitTransitionEnd',
|
|
||||||
MozTransition : 'transitionend',
|
|
||||||
OTransition : 'oTransitionEnd otransitionend',
|
|
||||||
transition : 'transitionend'
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var name in transEndEventNames) {
|
|
||||||
if (el.style[name] !== undefined) {
|
|
||||||
return { end: transEndEventNames[name] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false // explicit for ie8 ( ._.)
|
|
||||||
}
|
|
||||||
|
|
||||||
// http://blog.alexmaccaw.com/css-transitions
|
|
||||||
$.fn.emulateTransitionEnd = function (duration) {
|
|
||||||
var called = false
|
|
||||||
var $el = this
|
|
||||||
$(this).one('bsTransitionEnd', function () { called = true })
|
|
||||||
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
|
|
||||||
setTimeout(callback, duration)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
$.support.transition = transitionEnd()
|
|
||||||
|
|
||||||
if (!$.support.transition) return
|
|
||||||
|
|
||||||
$.event.special.bsTransitionEnd = {
|
|
||||||
bindType: $.support.transition.end,
|
|
||||||
delegateType: $.support.transition.end,
|
|
||||||
handle: function (e) {
|
|
||||||
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}(jQuery);
|
|
||||||
@@ -101,7 +101,10 @@ createQuery(Session& session,
|
|||||||
{
|
{
|
||||||
clauses.push_back("a.name LIKE ?");
|
clauses.push_back("a.name LIKE ?");
|
||||||
query.bind("%%" + keyword + "%%");
|
query.bind("%%" + keyword + "%%");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::string& keyword : keywords)
|
||||||
|
{
|
||||||
sortClauses.push_back("a.sort_name LIKE ?");
|
sortClauses.push_back("a.sort_name LIKE ?");
|
||||||
query.bind("%%" + keyword + "%%");
|
query.bind("%%" + keyword + "%%");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
#define LMS_DATABASE_VERSION 22
|
#define LMS_DATABASE_VERSION 23
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
|
|
||||||
@@ -254,6 +254,13 @@ CREATE TABLE "user_backup" (
|
|||||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(*this).modify()->incScanVersion();
|
ScanSettings::get(*this).modify()->incScanVersion();
|
||||||
}
|
}
|
||||||
|
else if (version == 22)
|
||||||
|
{
|
||||||
|
_session.execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
ScanSettings::get(*this).modify()->incScanVersion();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
void setDiscNumber(int num) { _discNumber = num; }
|
void setDiscNumber(int num) { _discNumber = num; }
|
||||||
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
|
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
|
||||||
void setTotalDisc(std::optional<int> totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; }
|
void setTotalDisc(std::optional<int> totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; }
|
||||||
|
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
|
||||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
@@ -113,6 +114,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
std::optional<std::size_t> getTrackNumber() const;
|
std::optional<std::size_t> getTrackNumber() const;
|
||||||
std::optional<std::size_t> getTotalTrack() const;
|
std::optional<std::size_t> getTotalTrack() const;
|
||||||
std::optional<std::size_t> getDiscNumber() const;
|
std::optional<std::size_t> getDiscNumber() const;
|
||||||
|
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||||
std::optional<std::size_t> getTotalDisc() const;
|
std::optional<std::size_t> getTotalDisc() const;
|
||||||
std::string getName() const { return _name; }
|
std::string getName() const { return _name; }
|
||||||
std::filesystem::path getPath() const { return _filePath; }
|
std::filesystem::path getPath() const { return _filePath; }
|
||||||
@@ -145,6 +147,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||||
Wt::Dbo::field(a, _discNumber, "disc_number");
|
Wt::Dbo::field(a, _discNumber, "disc_number");
|
||||||
|
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle");
|
||||||
Wt::Dbo::field(a, _totalTrack, "total_track");
|
Wt::Dbo::field(a, _totalTrack, "total_track");
|
||||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
@@ -177,6 +180,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
int _scanVersion {};
|
int _scanVersion {};
|
||||||
int _trackNumber {};
|
int _trackNumber {};
|
||||||
int _discNumber {};
|
int _discNumber {};
|
||||||
|
std::string _discSubtitle;
|
||||||
int _totalTrack {};
|
int _totalTrack {};
|
||||||
int _totalDisc {};
|
int _totalDisc {};
|
||||||
std::string _name;
|
std::string _name;
|
||||||
|
|||||||
@@ -211,6 +211,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
track.musicBrainzTrackID = UUID::fromString(value);
|
track.musicBrainzTrackID = UUID::fromString(value);
|
||||||
}
|
}
|
||||||
|
else if (tag == "TSST"
|
||||||
|
|| tag == "DISCSUBTITLE"
|
||||||
|
|| tag == "SETSUBTITLE")
|
||||||
|
{
|
||||||
|
track.discSubtitle = value;
|
||||||
|
}
|
||||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||||
{
|
{
|
||||||
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
|
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
|||||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||||
|
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
|
||||||
|
track.discSubtitle = value;
|
||||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||||
{
|
{
|
||||||
std::set<std::string> clusterNames;
|
std::set<std::string> clusterNames;
|
||||||
@@ -372,8 +374,12 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
if (mp3File->ID3v2Tag())
|
if (mp3File->ID3v2Tag())
|
||||||
{
|
{
|
||||||
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
|
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()};
|
||||||
|
|
||||||
|
if (!frameListMap["APIC"].isEmpty())
|
||||||
track.hasCover = true;
|
track.hasCover = true;
|
||||||
|
if (!frameListMap["TSST"].isEmpty())
|
||||||
|
properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPETags(mp3File->APETag());
|
getAPETags(mp3File->APETag());
|
||||||
|
|||||||
@@ -58,25 +58,26 @@ namespace MetaData
|
|||||||
{
|
{
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
std::vector<Artist> albumArtists;
|
std::vector<Artist> albumArtists;
|
||||||
std::string title;
|
std::string title;
|
||||||
std::optional<UUID> musicBrainzTrackID;
|
std::optional<UUID> musicBrainzTrackID;
|
||||||
std::optional<UUID> musicBrainzRecordID;
|
std::optional<UUID> musicBrainzRecordID;
|
||||||
std::optional<Album> album;
|
std::optional<Album> album;
|
||||||
Clusters clusters;
|
Clusters clusters;
|
||||||
std::chrono::milliseconds duration {};
|
std::chrono::milliseconds duration;
|
||||||
std::optional<std::size_t> trackNumber;
|
std::optional<std::size_t> trackNumber;
|
||||||
std::optional<std::size_t> totalTrack;
|
std::optional<std::size_t> totalTrack;
|
||||||
std::optional<std::size_t> discNumber;
|
std::optional<std::size_t> discNumber;
|
||||||
std::optional<std::size_t> totalDisc;
|
std::optional<std::size_t> totalDisc;
|
||||||
std::optional<int> year;
|
std::optional<int> year;
|
||||||
std::optional<int> originalYear;
|
std::optional<int> originalYear;
|
||||||
bool hasCover {false};
|
bool hasCover {};
|
||||||
std::vector<AudioStream> audioStreams;
|
std::vector<AudioStream> audioStreams;
|
||||||
std::optional<UUID> acoustID;
|
std::optional<UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
std::optional<float> trackReplayGain;
|
std::optional<float> trackReplayGain;
|
||||||
std::optional<float> albumReplayGain;
|
std::optional<float> albumReplayGain;
|
||||||
|
std::string discSubtitle;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IParser
|
class IParser
|
||||||
|
|||||||
@@ -738,6 +738,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
|||||||
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
||||||
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
||||||
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
||||||
|
if (!trackInfo->discSubtitle.empty())
|
||||||
|
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
|
||||||
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
||||||
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -141,11 +141,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
|||||||
|
|
||||||
// Require js here to avoid async problems
|
// Require js here to avoid async problems
|
||||||
requireJQuery("js/jquery-1.10.2.min.js");
|
requireJQuery("js/jquery-1.10.2.min.js");
|
||||||
require("js/mediaplayer.js");
|
|
||||||
require("js/bootstrap-notify.js");
|
require("js/bootstrap-notify.js");
|
||||||
require("js/collapse.js");
|
require("js/bootstrap.min.js");
|
||||||
require("js/dropdown.js");
|
require("js/mediaplayer.js");
|
||||||
require("js/transition.js");
|
|
||||||
|
|
||||||
setTitle("LMS");
|
setTitle("LMS");
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ LmsTheme::LmsTheme(Database::User::UITheme theme)
|
|||||||
void
|
void
|
||||||
LmsTheme::setTheme(Database::User::UITheme theme)
|
LmsTheme::setTheme(Database::User::UITheme theme)
|
||||||
{
|
{
|
||||||
|
if (theme == _theme)
|
||||||
|
return;
|
||||||
|
|
||||||
// Hack, use the application interface directly since changing theme is not allowed
|
// Hack, use the application interface directly since changing theme is not allowed
|
||||||
const auto currentStyleSheets {getStyleSheets(_theme)};
|
const auto currentStyleSheets {getStyleSheets(_theme)};
|
||||||
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ MediaPlayer::MediaPlayer()
|
|||||||
playNext {this, "playNext"},
|
playNext {this, "playNext"},
|
||||||
_settingsLoaded {this, "settingsLoaded"}
|
_settingsLoaded {this, "settingsLoaded"}
|
||||||
{
|
{
|
||||||
|
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
_title = bindNew<Wt::WText>("title");
|
_title = bindNew<Wt::WText>("title");
|
||||||
_artist = bindNew<Wt::WAnchor>("artist");
|
_artist = bindNew<Wt::WAnchor>("artist");
|
||||||
_release = bindNew<Wt::WAnchor>("release");
|
_release = bindNew<Wt::WAnchor>("release");
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Artist::createRelease(const Database::Artist::pointer& artist, const Release::po
|
|||||||
|
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||||
cover->setStyleClass("Lms-cover-medium");
|
cover->setStyleClass("Lms-cover");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
|
||||||
cover->setStyleClass("Lms-cover-smaller");
|
cover->setStyleClass("Lms-cover");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
|
|
||||||
auto artists = release->getReleaseArtists();
|
auto artists = release->getReleaseArtists();
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Release::refreshView()
|
|||||||
|
|
||||||
// Expect to be call in asc order
|
// Expect to be call in asc order
|
||||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||||
auto getOrAddDiscContainer = [&](std::size_t discNumber) -> Wt::WContainerWidget*
|
auto getOrAddDiscContainer = [&](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget*
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto it = trackContainers.find(discNumber);
|
auto it = trackContainers.find(discNumber);
|
||||||
@@ -178,9 +178,13 @@ Release::refreshView()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
|
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
|
||||||
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
|
||||||
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
if (discSubtitle.empty())
|
||||||
|
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
||||||
|
else
|
||||||
|
disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")};
|
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")};
|
||||||
trackContainers[discNumber] = tracksContainer;
|
trackContainers[discNumber] = tracksContainer;
|
||||||
|
|
||||||
@@ -198,7 +202,7 @@ Release::refreshView()
|
|||||||
|
|
||||||
Wt::WContainerWidget* container {rootContainer};
|
Wt::WContainerWidget* container {rootContainer};
|
||||||
if (isReleaseMultiDisc && discNumber)
|
if (isReleaseMultiDisc && discNumber)
|
||||||
container = getOrAddDiscContainer(*discNumber);
|
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
||||||
|
|
||||||
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
|
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ Releases::createEntry(const Release::pointer& release)
|
|||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
|
||||||
cover->setStyleClass("Lms-cover-medium");
|
cover->setStyleClass("Lms-cover");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
|
|
||||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ namespace UserInterface
|
|||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||||
cover->setStyleClass("Lms-cover-small");
|
cover->setStyleClass("Lms-cover");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ namespace UserInterface
|
|||||||
{
|
{
|
||||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||||
cover->setStyleClass("Lms-cover-small");
|
cover->setStyleClass("Lms-cover");
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ Tracks::createEntry(const Track::pointer& track)
|
|||||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||||
auto cover = std::make_unique<Wt::WImage>();
|
auto cover = std::make_unique<Wt::WImage>();
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
|
||||||
cover->setStyleClass("Lms-cover-small");
|
cover->setStyleClass("Lms-cover");
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(cover));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ Tracks::createEntry(const Track::pointer& track)
|
|||||||
{
|
{
|
||||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
|
||||||
cover->setStyleClass("Lms-cover-small");
|
cover->setStyleClass("Lms-cover");
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
|||||||
if (track->discNumber)
|
if (track->discNumber)
|
||||||
std::cout << "Disc: " << *track->discNumber << std::endl;
|
std::cout << "Disc: " << *track->discNumber << std::endl;
|
||||||
|
|
||||||
|
if (!track->discSubtitle.empty())
|
||||||
|
std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl;
|
||||||
|
|
||||||
if (track->totalDisc)
|
if (track->totalDisc)
|
||||||
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user