Merge branch 'develop' for release v3.14.0

This commit is contained in:
emeric
2020-05-13 14:22:01 +02:00
34 changed files with 137 additions and 522 deletions
+2 -1
View File
@@ -15,12 +15,13 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
* User management
* Recommendation engine
* Audio transcode for maximum interoperability and low bandwith requirements
* ReplayGain support
* Persistent play queue across sessions
* Compilation support
* Multi-value tags: artists, genres, ...
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
* MusicBrainzID support to handle duplicated artist and release names
* ReplayGain support
* Disc subtitles support
* _Systemd_ integration
* Subsonic API, with the following additional features:
* Playlists
+3 -1
View File
@@ -26,7 +26,9 @@
<div class="col-xs-12">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object Lms-cover"}
<div class="media-object Lms-cover-container-medium">
${cover}
</div>
</div>
<div class="media-body media-middle">
<h4 class="media-heading">
+1
View File
@@ -35,6 +35,7 @@
</div>
</div>
<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>
</div>
</div>
+3 -1
View File
@@ -82,7 +82,6 @@
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
<!--User-->
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Audio transcode bitrate limit</message>
<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-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">Search results</message>
<!--Player-->
<message id="Lms.Player.transcoding-active">Transcoding active</message>
<!--Playqueue-->
<message id="Lms.PlayQueue.clear">Clear</message>
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
+3 -1
View File
@@ -82,7 +82,6 @@
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
<!--User-->
<message id="Lms.Admin.User.audio-transcode-bitrate-limit">Limite du bitrate pour le transcodage</message>
<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-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">Résultats de la recherche</message>
<!--Player-->
<message id="Lms.Player.transcoding-active">Transcodage actif</message>
<!--Playqueue-->
<message id="Lms.PlayQueue.clear">Effacer</message>
<message id="Lms.PlayQueue.nb-tracks">{1} pistes</message>
+3 -1
View File
@@ -33,7 +33,9 @@
<div class="col-xs-9 Lms-playqueue-entry-name">
<div class="media">
<div class="media-left media-middle hidden-xs">
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
<div class="media-object Lms-cover-container-small">
${cover}
</div>
</div>
<div class="media-body media-middle">
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
+6 -2
View File
@@ -21,7 +21,9 @@
<div class="col-xs-12">
<div class="media">
<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 class="media-body media-middle">
<h4 class="media-heading">
@@ -40,7 +42,9 @@
<div class="col-xs-12">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object Lms-cover Lms-cover-container-smaller"}
<div class="media-object Lms-cover-container-smaller">
${cover}
</div>
</div>
<div class="media-body media-middle">
<div class="media-heading">
+3 -1
View File
@@ -25,7 +25,9 @@
<div class="col-xs-12 Lms-explore-tracks-entry-name">
<div class="media">
<div class="media-left media-middle hidden-xs">
${cover class="media-object Lms-cover"}
<div class="media-object Lms-cover-container-small">
${cover}
</div>
</div>
<div class="media-body media-middle">
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
+3 -1
View File
@@ -26,7 +26,9 @@
<div class="col-xs-9 Lms-explore-tracks-entry-name">
<div class="media">
<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 class="media-body media-middle">
<h4><div class="Lms-trackname text-primary">${name}</div></h4>
-20
View File
@@ -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
View File
@@ -177,7 +177,8 @@ RUN \
## Release Stage
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=" \
openssl \
+25 -32
View File
@@ -62,35 +62,41 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
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 {
filter: brightness(130%);
}
.Lms-cover-container-smaller {
}
.Lms-cover-smaller {
width: 64px;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
height: 64px;
position: relative;
}
.Lms-cover-container-small {
}
.Lms-cover-small {
width: 96px;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
height: 96px;
position: relative;
}
.Lms-cover-container-medium {
}
.Lms-cover-medium {
width: 128px;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
height: 128px;
position: relative;
}
.Lms-cover-large {
@@ -119,8 +125,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
}
.Lms-explore-releases-entry {
padding-top: 8px;
padding-bottom: 8px;
padding-top: 4px;
padding-bottom: 4px;
}
.Lms-explore-release-cover {
@@ -181,8 +187,8 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
}
.Lms-explore-artist-entry {
padding-top: 8px;
padding-bottom: 8px;
padding-top: 4px;
padding-bottom: 4px;
}
.Lms-explore-artist-entries-container {
@@ -193,10 +199,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
margin-bottom: 8px;
}
.Lms-explore-tracks-entry {
min-height: 32px;
}
.Lms-explore-tracks-entry:hover .Lms-explore-tracks-entry-controls {
display: block;
}
@@ -221,11 +223,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
display: block;
}
.Lms-explore-tracksinfo-entry {
min-height: 28px;
margin: 0;
}
.Lms-header {
padding-bottom: 9px;
margin-top: 20px;
@@ -248,10 +245,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
text-align: left;
}
.Lms-playqueue-entry {
min-height: 32px;
}
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
display: block;
}
Vendored Executable
+7
View File
File diff suppressed because one or more lines are too long
-211
View File
@@ -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);
-165
View File
@@ -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);
+12
View File
@@ -137,6 +137,9 @@ LMS.mediaplayer = function () {
_elems.duration = document.getElementById("lms-mp-duration");
_elems.volume = document.getElementById("lms-mp-volume");
_elems.volumeslider = document.getElementById("lms-mp-volume-slider");
_elems.transcodingActive = document.getElementById("lms-transcoding-active");
$(_elems.transcodingActive).tooltip()
var source = _audioCtx.createMediaElementSource(_elems.audio);
source.connect(_gainNode);
@@ -198,6 +201,15 @@ LMS.mediaplayer = function () {
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();
_initDefaultSettings(defaultSettings);
-59
View File
@@ -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);
+3
View File
@@ -101,7 +101,10 @@ createQuery(Session& session,
{
clauses.push_back("a.name LIKE ?");
query.bind("%%" + keyword + "%%");
}
for (const std::string& keyword : keywords)
{
sortClauses.push_back("a.sort_name LIKE ?");
query.bind("%%" + keyword + "%%");
}
+8 -1
View File
@@ -40,7 +40,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 22
#define LMS_DATABASE_VERSION 23
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
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
{
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 setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 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 setDuration(std::chrono::milliseconds duration) { _duration = duration; }
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> getTotalTrack() const;
std::optional<std::size_t> getDiscNumber() const;
const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::optional<std::size_t> getTotalDisc() const;
std::string getName() const { return _name; }
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, _trackNumber, "track_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, _totalDisc, "total_disc");
Wt::Dbo::field(a, _name, "name");
@@ -177,6 +180,7 @@ class Track : public Wt::Dbo::Dbo<Track>
int _scanVersion {};
int _trackNumber {};
int _discNumber {};
std::string _discSubtitle;
int _totalTrack {};
int _totalDisc {};
std::string _name;
@@ -211,6 +211,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
{
track.musicBrainzTrackID = UUID::fromString(value);
}
else if (tag == "TSST"
|| tag == "DISCSUBTITLE"
|| tag == "SETSUBTITLE")
{
track.discSubtitle = value;
}
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
+7 -1
View File
@@ -270,6 +270,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
track.albumReplayGain = StringUtils::readAs<float>(value);
else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.trackReplayGain = StringUtils::readAs<float>(value);
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
track.discSubtitle = value;
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
@@ -372,8 +374,12 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
{
if (mp3File->ID3v2Tag())
{
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()};
if (!frameListMap["APIC"].isEmpty())
track.hasCover = true;
if (!frameListMap["TSST"].isEmpty())
properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString());
}
getAPETags(mp3File->APETag());
+10 -9
View File
@@ -58,25 +58,26 @@ namespace MetaData
{
std::vector<Artist> artists;
std::vector<Artist> albumArtists;
std::string title;
std::string title;
std::optional<UUID> musicBrainzTrackID;
std::optional<UUID> musicBrainzRecordID;
std::optional<Album> album;
Clusters clusters;
std::chrono::milliseconds duration {};
std::optional<Album> album;
Clusters clusters;
std::chrono::milliseconds duration;
std::optional<std::size_t> trackNumber;
std::optional<std::size_t> totalTrack;
std::optional<std::size_t> discNumber;
std::optional<std::size_t> totalDisc;
std::optional<int> year;
std::optional<int> originalYear;
bool hasCover {false};
bool hasCover {};
std::vector<AudioStream> audioStreams;
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain;
std::string copyright;
std::string copyrightURL;
std::optional<float> trackReplayGain;
std::optional<float> albumReplayGain;
std::string discSubtitle;
};
class IParser
+2
View File
@@ -738,6 +738,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
track.modify()->setTotalTrack(trackInfo->totalTrack);
track.modify()->setTotalDisc(trackInfo->totalDisc);
if (!trackInfo->discSubtitle.empty())
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
+2 -4
View File
@@ -141,11 +141,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
// Require js here to avoid async problems
requireJQuery("js/jquery-1.10.2.min.js");
require("js/mediaplayer.js");
require("js/bootstrap-notify.js");
require("js/collapse.js");
require("js/dropdown.js");
require("js/transition.js");
require("js/bootstrap.min.js");
require("js/mediaplayer.js");
setTitle("LMS");
+3
View File
@@ -34,6 +34,9 @@ LmsTheme::LmsTheme(Database::User::UITheme theme)
void
LmsTheme::setTheme(Database::User::UITheme theme)
{
if (theme == _theme)
return;
// Hack, use the application interface directly since changing theme is not allowed
const auto currentStyleSheets {getStyleSheets(_theme)};
for (auto it {std::crbegin(currentStyleSheets)}; it != std::crend(currentStyleSheets); ++it)
+2
View File
@@ -196,6 +196,8 @@ MediaPlayer::MediaPlayer()
playNext {this, "playNext"},
_settingsLoaded {this, "settingsLoaded"}
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
_title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist");
_release = bindNew<Wt::WAnchor>("release");
+1 -1
View File
@@ -144,7 +144,7 @@ Artist::createRelease(const Database::Artist::pointer& artist, const Release::po
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
cover->setStyleClass("Lms-cover-medium");
cover->setStyleClass("Lms-cover");
anchor->setImage(std::move(cover));
}
+1 -1
View File
@@ -43,7 +43,7 @@ namespace UserInterface::ReleaseListHelpers
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 64));
cover->setStyleClass("Lms-cover-smaller");
cover->setStyleClass("Lms-cover");
anchor->setImage(std::move(cover));
auto artists = release->getReleaseArtists();
+7 -3
View File
@@ -169,7 +169,7 @@ Release::refreshView()
// Expect to be call in asc order
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);
@@ -178,9 +178,13 @@ Release::refreshView()
}
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);
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")};
trackContainers[discNumber] = tracksContainer;
@@ -198,7 +202,7 @@ Release::refreshView()
Wt::WContainerWidget* container {rootContainer};
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"))};
+1 -1
View File
@@ -117,7 +117,7 @@ Releases::createEntry(const Release::pointer& release)
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 128));
cover->setStyleClass("Lms-cover-medium");
cover->setStyleClass("Lms-cover");
anchor->setImage(std::move(cover));
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
+2 -2
View File
@@ -72,7 +72,7 @@ namespace UserInterface
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
cover->setStyleClass("Lms-cover-small");
cover->setStyleClass("Lms-cover");
anchor->setImage(std::move(cover));
}
}
@@ -80,7 +80,7 @@ namespace UserInterface
{
auto cover = entry->bindNew<Wt::WImage>("cover");
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, 96));
cover->setStyleClass("Lms-cover-small");
cover->setStyleClass("Lms-cover");
}
return entry;
+2 -2
View File
@@ -229,7 +229,7 @@ Tracks::createEntry(const Track::pointer& track)
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
auto cover = std::make_unique<Wt::WImage>();
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 96));
cover->setStyleClass("Lms-cover-small");
cover->setStyleClass("Lms-cover");
anchor->setImage(std::move(cover));
}
}
@@ -237,7 +237,7 @@ Tracks::createEntry(const Track::pointer& track)
{
auto cover = entry->bindNew<Wt::WImage>("cover");
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);
+3
View File
@@ -110,6 +110,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
if (track->discNumber)
std::cout << "Disc: " << *track->discNumber << std::endl;
if (!track->discSubtitle.empty())
std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl;
if (track->totalDisc)
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;