Merge branch 'develop' for release v3.19.0
This commit is contained in:
+2
-15
@@ -1,25 +1,12 @@
|
||||
language: cpp
|
||||
sudo: required
|
||||
dist: bionic
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-9
|
||||
env:
|
||||
- MATRIX_EVAL="CC=gcc-9 && CXX=g++-9"
|
||||
compiler:
|
||||
- gcc
|
||||
dist: focal
|
||||
before_install:
|
||||
- eval "${MATRIX_EVAL}"
|
||||
- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libgraphicsmagick++1-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev libpam0g-dev
|
||||
- git clone https://github.com/emweb/wt.git wt
|
||||
- pushd wt;
|
||||
- git checkout 4.2.1
|
||||
- git checkout 4.4.0
|
||||
- cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && sudo make install
|
||||
- popd
|
||||
script: cmake -DCMAKE_BUILD_TYPE=Release . && make && make test
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ find_package(PkgConfig REQUIRED)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(FFMPEGAV REQUIRED)
|
||||
find_package(Taglib REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options)
|
||||
find_package(PStreams REQUIRED)
|
||||
find_package(PAM)
|
||||
pkg_check_modules(GRAPHICSMAGICKXX REQUIRED GraphicsMagick++)
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
<div class="col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.links}</h3>
|
||||
${mbid-link class="text-muted"}
|
||||
${mbid-link class="Lms-btn text-muted"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-mbid>}
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-9 col-md-10 Lms-explore-list-entry-text-container">
|
||||
<h5>${name class="Lms-releasename text-primary"}</h5>
|
||||
${<if-has-artist>}<h6>${artist class="Lms-artistname text-muted"}</h6>${</if-has-artist>}
|
||||
${<if-has-artist>}<h6>${artist class="Lms-btn Lms-artistname text-muted"}</h6>${</if-has-artist>}
|
||||
<h6><small><div class="text-muted">${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</div></small></h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-3
@@ -4,7 +4,6 @@
|
||||
|
||||
<message id="Lms.Explore.Artists.template">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.artists}</h2>
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
@@ -18,13 +17,13 @@
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry">
|
||||
<div class="Lms-explore-artists-entry Lms-entry-hoverable Lms-vertical-align">
|
||||
<h4>${name class="Lms-artistname text-primary"}</h4>
|
||||
<h4>${name class="Lms-btn Lms-artistname text-primary"}</h4>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry-small">
|
||||
<div class="Lms-explore-artists-entry-small Lms-entry-hoverable Lms-vertical-align">
|
||||
${name class="Lms-artistname text-primary"}
|
||||
${name class="Lms-btn Lms-artistname text-primary"}
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
${contents class="Lms-explore-contents"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.more-btn"><i class="fa fa-plus-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-play-circle Lms-explore-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.Explore.template.more-btn"><i class="fa fa-plus-circle Lms-explore-btn Lms-btn"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-play-circle Lms-explore-btn Lms-btn"></i></message>
|
||||
|
||||
<message id="Lms.Explore.template.filters">
|
||||
${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"}
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
<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"/>
|
||||
<div class="progress Lms-player-seek-common Lms-player-progress">
|
||||
<div id="lms-mp-progress" class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
|
||||
<div id="lms-mp-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-5 col-sm-2 Lms-player-controls">
|
||||
<i id="lms-mp-previous" class="fa fa-step-backward Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i><i id="lms-mp-playpause" class="fa fa-play Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i><i id="lms-mp-next" class="fa fa-step-forward Lms-player-btn Lms-control-btn Lms-player-btn-controls"></i>
|
||||
<i id="lms-mp-previous" class="fa fa-step-backward Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-playpause" class="fa fa-play Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-next" class="fa fa-step-forward Lms-player-btn Lms-btn Lms-player-btn-controls"></i>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2">
|
||||
<div class="Lms-player-volume">
|
||||
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-control-btn Lms-player-btn-volume"></i>
|
||||
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up Lms-player-btn Lms-btn Lms-player-btn-volume"></i>
|
||||
<span class="hidden-xs Lms-player-volume-slider-container">
|
||||
<input id ="lms-mp-volume-slider" type="range" min="0" max="1" step="0.01" value="0.8" class="Lms-player-volume-slider"/>
|
||||
</span>
|
||||
@@ -31,11 +31,11 @@
|
||||
${title class="Lms-player-text Lms-trackname text-primary"}
|
||||
</div>
|
||||
<div class="Lms-player-text-center">
|
||||
<span class="hidden-xs">${release class="Lms-player-text Lms-releasename text-primary"}</span><small>${artist class="Lms-player-text Lms-artistname text-muted"}</small>
|
||||
<span class="hidden-xs">${release class="Lms-btn Lms-player-text Lms-releasename text-primary"}</span><small>${artist class="Lms-btn Lms-player-text Lms-artistname text-muted"}</small>
|
||||
</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="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></a>
|
||||
<a id="lms-transcoding-active" class="Lms-player-btn Lms-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="visibility:hidden"><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>
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||
<message id="Lms.Explore.all">All</message>
|
||||
<message id="Lms.Explore.artists">Artists</message>
|
||||
<message id="Lms.Explore.download">Download</message>
|
||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||
<message id="Lms.Explore.filters">Filters</message>
|
||||
<message id="Lms.Explore.links">Links</message>
|
||||
@@ -127,8 +128,11 @@
|
||||
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.star">Star</message>
|
||||
<message id="Lms.Explore.starred">Starred</message>
|
||||
<message id="Lms.Explore.tracks">Tracks</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
<message id="Lms.Explore.unstar">Unstar</message>
|
||||
<message id="Lms.Explore.value">Value</message>
|
||||
<message id="Lms.Explore.various-artists">Various artists</message>
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
||||
<message id="Lms.Explore.all">Tous</message>
|
||||
<message id="Lms.Explore.artists">Artistes</message>
|
||||
<message id="Lms.Explore.download">Télécharger</message>
|
||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||
<message id="Lms.Explore.filters">Filtres</message>
|
||||
<message id="Lms.Explore.links">Liens</message>
|
||||
@@ -127,8 +128,11 @@
|
||||
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
|
||||
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||
<message id="Lms.Explore.starred">Favoris</message>
|
||||
<message id="Lms.Explore.tracks">Pistes</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
<message id="Lms.Explore.unstar">Retirer des favoris</message>
|
||||
<message id="Lms.Explore.value">Valeur</message>
|
||||
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
||||
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
<!--FORMS message blocks-->
|
||||
|
||||
<message id="Lms.PlayQueue.template.clear-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.clear-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random Lms-playqueue-btn Lms-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat Lms-playqueue-btn Lms-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss Lms-playqueue-btn Lms-btn"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
|
||||
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
||||
<h4><small>${nb-tracks}</small></h4>
|
||||
</div>
|
||||
@@ -21,8 +20,8 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play-circle Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-control-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.play-btn"><i class="fa fa-fw fa-play-circle Lms-playqueue-btn Lms-btn"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times-circle Lms-playqueue-btn Lms-btn"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry">
|
||||
<div class="row Lms-vertical-align Lms-explore-entry-list Lms-playqueue-entry ${is-selected}">
|
||||
@@ -34,7 +33,7 @@
|
||||
<div class="col-xs-7 col-sm-8 col-md-8 Lms-explore-entry-list-text-container">
|
||||
<div class="Lms-trackname text-primary"><h5>${name}</h5></div>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-btn Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
</div>
|
||||
@@ -50,7 +49,7 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry-artist">
|
||||
<small>${artist class="Lms-artistname text-muted"}</small>
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
<div class="col-md-8">
|
||||
<div class="Lms-header">
|
||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||
${<if-has-artist>}<h4>${artist class="Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
||||
${<if-has-artist>}<h4>${artist class="Lms-btn Lms-artistname text-muted"}</h4>${</if-has-artist>}
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
|
||||
${clusters}
|
||||
${play-btn}${more-btn}
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<div class="well well-sm">
|
||||
<h3>${tr:Lms.Explore.links}</h3>
|
||||
${mbid-link class="text-muted"}
|
||||
${mbid-link class="Lms-btn text-muted"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-mbid>}
|
||||
@@ -78,7 +78,7 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-artist">
|
||||
<small>${artist class="Lms-artistname text-muted"}</small>
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
<message id="Lms.Explore.Releases.template">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.releases}</h2>
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${more-btn}
|
||||
</div>
|
||||
@@ -21,8 +20,8 @@
|
||||
${cover}
|
||||
</div>
|
||||
<div class="Lms-explore-entry-grid-text-container">
|
||||
<h5>${release-name class="Lms-releasename text-primary"}</h5>
|
||||
<div class="Lms-explore-entry-separator"><h6>${<if-has-year>}<small><span class="text-muted">${year}</span></small>${</if-has-year>}${<if-has-artist>}${artist-name class="Lms-artistname text-muted"}${</if-has-artist>}</h6></div>
|
||||
<h5>${release-name class="Lms-btn Lms-releasename text-primary"}</h5>
|
||||
<div class="Lms-explore-entry-separator"><h6>${<if-has-year>}<small><span class="text-muted">${year}</span></small>${</if-has-year>}${<if-has-artist>}${artist-name class="Lms-btn Lms-artistname text-muted"}${</if-has-artist>}</h6></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
${title class="navbar-brand"}
|
||||
${title class="navbar-brand Lms-btn"}
|
||||
<div class="Lms-navbar-search pull-left">
|
||||
${search class="form-control"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>${artists}</li>
|
||||
<li>${releases}</li>
|
||||
<li>${tracks}</li>
|
||||
<li>${playqueue}</li>
|
||||
<li>${artists class="Lms-btn"}</li>
|
||||
<li>${releases class="Lms-btn"}</li>
|
||||
<li>${tracks class="Lms-btn"}</li>
|
||||
<li>${playqueue class="Lms-btn"}</li>
|
||||
</ul>
|
||||
<div class="navbar-form navbar-left">
|
||||
${filters class="form-group Lms-clusters-control"}
|
||||
@@ -29,7 +29,7 @@
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
${<if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.administration}"><i class="fa fa-cogs" aria-hidden="true"></i> <span class="caret"></span></a>
|
||||
<a href="#" class="Lms-btn dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.administration}"><i class="fa fa-cogs" aria-hidden="true"></i> <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li>${database}</li>
|
||||
<li>${users}</li>
|
||||
@@ -37,7 +37,7 @@
|
||||
</li>
|
||||
${</if-is-admin>}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.user}"><i class="fa fa-user" aria-hidden="true"></i>
|
||||
<a href="#" class="Lms-btn dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" title="${tr:Lms.user}"><i class="fa fa-user" aria-hidden="true"></i>
|
||||
<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="dropdown-header">${username}</li>
|
||||
|
||||
+2
-3
@@ -7,7 +7,6 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="Lms-header">
|
||||
<h2>${tr:Lms.Explore.tracks}</h2>
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${more-btn}
|
||||
</div>
|
||||
@@ -27,7 +26,7 @@
|
||||
<div class="col-xs-7 col-sm-8 col-md-8 Lms-explore-entry-list-text-container">
|
||||
<div class="Lms-trackname text-primary"><h5>${name}</h5></div>
|
||||
${<if-has-artists-or-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-release>}<small>${release class="Lms-btn Lms-releasename text-primary"}</small>${</if-has-release>}
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
${</if-has-artists-or-release>}
|
||||
</div>
|
||||
@@ -43,7 +42,7 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.entry-artist">
|
||||
<small>${artist class="Lms-artistname text-muted"}</small>
|
||||
<small>${artist class="Lms-btn Lms-artistname text-muted"}</small>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -19,10 +19,6 @@ a.Lms-artistname.text-primary:hover {
|
||||
color: var(--hover-color)
|
||||
}
|
||||
|
||||
.Lms-control-btn {
|
||||
color: var(--body-color)
|
||||
}
|
||||
|
||||
.Lms-releasename.text-primary, .Lms-releasename.text-primary:hover {
|
||||
color: var(--body-color)
|
||||
}
|
||||
|
||||
+16
-5
@@ -41,6 +41,15 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.Lms-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-btn:hover {
|
||||
color: var(--hover-color);
|
||||
transition: color .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
}
|
||||
|
||||
.Lms-cluster {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
@@ -51,17 +60,13 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
|
||||
.Lms-cluster:hover {
|
||||
filter: brightness(130%);
|
||||
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
}
|
||||
|
||||
.Lms-clusters-control {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.Lms-control-btn:hover {
|
||||
color: var(--hover-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-cover {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
@@ -84,6 +89,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
|
||||
.Lms-cover:hover {
|
||||
filter: brightness(130%);
|
||||
transition: filter .20s cubic-bezier(.43,-.1,.16,1.1);
|
||||
}
|
||||
|
||||
.Lms-cover-container {
|
||||
@@ -268,6 +274,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
max-width: 175px;
|
||||
padding-top: 7.5px;
|
||||
padding-bottom: 7.5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-btn {
|
||||
@@ -362,6 +369,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
background-color: var(--slider-background-color);
|
||||
}
|
||||
|
||||
.Lms-player-progress .progress-bar {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.Lms-player-seek {
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
@@ -31,12 +31,10 @@ LMS.mediaplayer = function () {
|
||||
if (_elems.audio.paused) {
|
||||
_elems.playpause.classList.remove("fa-pause");
|
||||
_elems.playpause.classList.add("fa-play");
|
||||
_elems.progress.classList.remove("active");
|
||||
}
|
||||
else {
|
||||
_elems.playpause.classList.remove("fa-play");
|
||||
_elems.playpause.classList.add("fa-pause");
|
||||
_elems.progress.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +238,10 @@ LMS.mediaplayer = function () {
|
||||
document.addEventListener("keydown", function(event) {
|
||||
let handled = false;
|
||||
|
||||
if (event.keyCode == 32 && !(event.target instanceof HTMLInputElement)) {
|
||||
if (event.target instanceof HTMLInputElement)
|
||||
return;
|
||||
|
||||
if (event.keyCode == 32) {
|
||||
_playPause();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ static std::filesystem::path ffmpegPath;
|
||||
void
|
||||
Transcoder::init()
|
||||
{
|
||||
ffmpegPath = ServiceProvider<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(ffmpegPath))
|
||||
throw LmsException {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||
}
|
||||
|
||||
@@ -303,7 +303,11 @@ Artist::getByFilter(Session& session,
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<TrackArtistLink::Type> linkType, std::optional<Range> range, bool& moreResults)
|
||||
Artist::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -330,6 +334,58 @@ Artist::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, con
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Artist::pointer>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN user_artist_starred uas ON uas.artist_id = a.id"
|
||||
" INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> collection = query
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>>
|
||||
Artist::getReleases(const std::set<IdType>& clusterIds) const
|
||||
{
|
||||
|
||||
@@ -43,6 +43,13 @@ Db::Db(const std::filesystem::path& dbPath)
|
||||
_connectionPool = std::move(connectionPool);
|
||||
}
|
||||
|
||||
Db::~Db()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
||||
executeSql("pragma optimize");
|
||||
LMS_LOG(DB, DEBUG) << "Optimizing db DONE";
|
||||
}
|
||||
|
||||
void
|
||||
Db::executeSql(const std::string& sql)
|
||||
{
|
||||
|
||||
@@ -245,6 +245,46 @@ Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<std
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::set<IdType>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Release::pointer>(session, "SELECT r from release r", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN user_release_starred urs ON urs.release_id = r.id"
|
||||
" INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Release::pointer> collection = query
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 25
|
||||
#define LMS_DATABASE_VERSION 26
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -272,6 +272,12 @@ CREATE TABLE "user_backup" (
|
||||
// User's AuthMode
|
||||
_session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultAuthMode)) + ")");
|
||||
}
|
||||
else if (version == 25)
|
||||
{
|
||||
// Better cover detection
|
||||
// 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";
|
||||
|
||||
@@ -265,6 +265,41 @@ Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusterIds,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Track::pointer>(session, "SELECT t from track t", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN user_track_starred uts ON uts.track_id = t.id"
|
||||
" INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user.id());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Track::pointer> collection = query
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1);
|
||||
|
||||
auto res {std::vector<pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Track::getClusters() const
|
||||
{
|
||||
|
||||
@@ -172,12 +172,6 @@ User::hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const
|
||||
return _starredArtists.count(artist) != 0;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>>
|
||||
User::getStarredArtists() const
|
||||
{
|
||||
return std::vector<Wt::Dbo::ptr<Artist>>(_starredArtists.begin(), _starredArtists.end());
|
||||
}
|
||||
|
||||
void
|
||||
User::starRelease(Wt::Dbo::ptr<Release> release)
|
||||
{
|
||||
@@ -198,17 +192,6 @@ User::hasStarredRelease(Wt::Dbo::ptr<Release> release) const
|
||||
return _starredReleases.count(release) != 0;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>>
|
||||
User::getStarredReleases(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
|
||||
{
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> res = _starredReleases.find()
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<Wt::Dbo::ptr<Release>>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
User::starTrack(Wt::Dbo::ptr<Track> track)
|
||||
{
|
||||
@@ -229,12 +212,6 @@ User::hasStarredTrack(Wt::Dbo::ptr<Track> track) const
|
||||
return _starredTracks.count(track) != 0;
|
||||
}
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>>
|
||||
User::getStarredTracks() const
|
||||
{
|
||||
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
|
||||
@@ -86,6 +86,12 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range>, bool& moreResults);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
|
||||
@@ -32,6 +32,12 @@ class Db
|
||||
public:
|
||||
|
||||
Db(const std::filesystem::path& dbPath);
|
||||
~Db();
|
||||
|
||||
Db(const Db&) = delete;
|
||||
Db(Db&&) = delete;
|
||||
Db& operator=(const Db&) = delete;
|
||||
Db& operator=(Db&&) = delete;
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
|
||||
@@ -59,6 +59,7 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
static std::vector<IdType> getAllIdsRandom(Session& session, const std::set<IdType>& clusters, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<pointer> getByYear(Session& session, int yearFrom, int yearTo, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getStarred(Session& session, Wt::Dbo::ptr<User> user, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
|
||||
|
||||
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
|
||||
@@ -81,6 +81,10 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
Wt::Dbo::ptr<User> user,
|
||||
const std::set<IdType>& clusters,
|
||||
std::optional<Range> range, bool& hasMore);
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, const std::filesystem::path& p);
|
||||
|
||||
@@ -197,18 +197,15 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
void starArtist(Wt::Dbo::ptr<Artist> artist);
|
||||
void unstarArtist(Wt::Dbo::ptr<Artist> artist);
|
||||
bool hasStarredArtist(Wt::Dbo::ptr<Artist> artist) const;
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getStarredArtists() const;
|
||||
|
||||
void starRelease(Wt::Dbo::ptr<Release> release);
|
||||
void unstarRelease(Wt::Dbo::ptr<Release> release);
|
||||
bool hasStarredRelease(Wt::Dbo::ptr<Release> release) const;
|
||||
std::vector<Wt::Dbo::ptr<Release>> getStarredReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
|
||||
|
||||
// Stars
|
||||
void starTrack(Wt::Dbo::ptr<Track> track);
|
||||
void unstarTrack(Wt::Dbo::ptr<Track> track);
|
||||
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
|
||||
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
@@ -386,6 +387,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
//MP4
|
||||
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())})
|
||||
{
|
||||
auto& coverItem {mp4File->tag()->itemListMap()["covr"]};
|
||||
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
|
||||
if (!coverArtList.isEmpty())
|
||||
track.hasCover = true;
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())})
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
|
||||
@@ -38,6 +38,12 @@ createEngine(Database::Db& db)
|
||||
Engine::Engine(Database::Db& db)
|
||||
: _dbSession {db}
|
||||
{
|
||||
start();
|
||||
}
|
||||
|
||||
Engine::~Engine()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -45,9 +51,6 @@ Engine::start()
|
||||
{
|
||||
assert(!_running);
|
||||
_running = true;
|
||||
|
||||
requestReloadInternal(false);
|
||||
|
||||
_ioService.start();
|
||||
}
|
||||
|
||||
@@ -62,6 +65,12 @@ Engine::stop()
|
||||
_ioService.stop();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::requestLoad()
|
||||
{
|
||||
requestReloadInternal(false);
|
||||
}
|
||||
|
||||
void
|
||||
Engine::requestReload()
|
||||
{
|
||||
|
||||
@@ -35,12 +35,19 @@ namespace Recommendation
|
||||
{
|
||||
public:
|
||||
Engine(Database::Db& db);
|
||||
~Engine();
|
||||
|
||||
Engine(const Engine&) = delete;
|
||||
Engine(Engine&&) = delete;
|
||||
Engine& operator=(const Engine&) = delete;
|
||||
Engine& operator=(Engine&&) = delete;
|
||||
|
||||
private:
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
void requestLoad() override;
|
||||
void requestReload() override;
|
||||
Wt::Signal<>& reloaded() override { return _sigReloaded; }
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Recommendation {
|
||||
static
|
||||
std::filesystem::path getCacheDirectory()
|
||||
{
|
||||
return ServiceProvider<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||
}
|
||||
|
||||
static std::filesystem::path getCacheNetworkFilePath()
|
||||
@@ -237,7 +237,7 @@ FeaturesClassifierCache::read()
|
||||
void
|
||||
FeaturesClassifierCache::write() const
|
||||
{
|
||||
std::filesystem::create_directories(ServiceProvider<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
|
||||
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
||||
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
||||
|
||||
@@ -40,8 +40,7 @@ namespace Recommendation
|
||||
public:
|
||||
virtual ~IEngine() = default;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void requestLoad() = 0;
|
||||
|
||||
virtual void requestReload() = 0;
|
||||
virtual Wt::Signal<>& reloaded() = 0;
|
||||
|
||||
@@ -40,7 +40,7 @@ getJsonData(const UUID& mbid)
|
||||
{
|
||||
static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/";
|
||||
|
||||
const std::string url {ServiceProvider<IConfig>::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"};
|
||||
const std::string url {Service<IConfig>::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"};
|
||||
|
||||
boost::asio::io_service ioService;
|
||||
|
||||
|
||||
@@ -254,6 +254,8 @@ MediaScanner::MediaScanner(Database::Db& db)
|
||||
_ioService.setThreadCount(1);
|
||||
|
||||
refreshScanSettings();
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
MediaScanner::~MediaScanner()
|
||||
|
||||
@@ -50,8 +50,6 @@ class MediaScanner : public IMediaScanner
|
||||
MediaScanner& operator=(const MediaScanner&) = delete;
|
||||
MediaScanner& operator=(MediaScanner&&) = delete;
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void requestReload() override;
|
||||
void requestImmediateScan(bool force) override;
|
||||
|
||||
@@ -64,6 +62,9 @@ class MediaScanner : public IMediaScanner
|
||||
|
||||
private:
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
// Job handling
|
||||
void scheduleNextScan();
|
||||
void scheduleScan(bool force, const Wt::WDateTime& dateTime = {});
|
||||
|
||||
@@ -38,14 +38,10 @@ class IMediaScanner
|
||||
public:
|
||||
virtual ~IMediaScanner() = default;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
// Async requests
|
||||
virtual void requestReload() = 0;
|
||||
virtual void requestImmediateScan(bool force) = 0;
|
||||
|
||||
|
||||
enum class State
|
||||
{
|
||||
NotScheduled,
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace API::Subsonic::Scan
|
||||
{
|
||||
Response::Node statusResponse;
|
||||
|
||||
const IMediaScanner::Status scanStatus {ServiceProvider<IMediaScanner>::get()->getStatus()};
|
||||
const IMediaScanner::Status scanStatus {Service<IMediaScanner>::get()->getStatus()};
|
||||
|
||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress);
|
||||
if (scanStatus.currentState == IMediaScanner::State::InProgress)
|
||||
@@ -61,7 +61,7 @@ namespace API::Subsonic::Scan
|
||||
Response
|
||||
handleStartScan(RequestContext& context)
|
||||
{
|
||||
ServiceProvider<IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<IMediaScanner>::get()->requestImmediateScan(false);
|
||||
|
||||
Response response {Response::createOkResponse(context)};
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
@@ -511,10 +511,10 @@ handleChangePassword(RequestContext& context)
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
|
||||
if (!ServiceProvider<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
throw PasswordTooWeakGenericError {};
|
||||
|
||||
const User::PasswordHash hash {ServiceProvider<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
@@ -593,10 +593,10 @@ handleCreateUserRequest(RequestContext& context)
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
if (!ServiceProvider<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
throw PasswordTooWeakGenericError {};
|
||||
|
||||
const User::PasswordHash hash {ServiceProvider<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
@@ -743,7 +743,8 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
}
|
||||
else if (type == "starred")
|
||||
{
|
||||
releases = user->getStarredReleases(offset, size);
|
||||
bool moreResults {};
|
||||
releases = Release::getStarred(context.dbSession, user, {}, Range {offset, size}, moreResults);
|
||||
}
|
||||
else if (type == "byGenre")
|
||||
{
|
||||
@@ -878,7 +879,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
||||
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
||||
}
|
||||
|
||||
auto similarArtistsId {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, count)};
|
||||
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, count)};
|
||||
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
@@ -1107,7 +1108,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
// Optional params
|
||||
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
|
||||
|
||||
auto similarArtistsId {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, 5)};
|
||||
auto similarArtistsId {Service<Recommendation::IEngine>::get()->getSimilarArtists(context.dbSession, id.value, 5)};
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -1172,19 +1173,22 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
|
||||
|
||||
{
|
||||
auto artists {user->getStarredArtists()};
|
||||
bool moreResults {};
|
||||
const auto artists {Artist::getStarred(context.dbSession, user, {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, moreResults)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto releases {user->getStarredReleases()};
|
||||
bool moreResults {};
|
||||
const auto releases {Release::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)};
|
||||
for (const Release::pointer& release : releases)
|
||||
starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto tracks {user->getStarredTracks()};
|
||||
bool moreResults {};
|
||||
const auto tracks {Track::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
@@ -1552,10 +1556,10 @@ handleUpdateUserRequest(RequestContext& context)
|
||||
if (password)
|
||||
{
|
||||
*password = decodePasswordIfNeeded(*password);
|
||||
if (!ServiceProvider<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, *password))
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, *password))
|
||||
throw PasswordTooWeakGenericError {};
|
||||
|
||||
hash = ServiceProvider<Auth::IPasswordService>::get()->hashPassword(*password);
|
||||
hash = Service<Auth::IPasswordService>::get()->hashPassword(*password);
|
||||
}
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
@@ -1746,10 +1750,10 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/,
|
||||
switch (id.type)
|
||||
{
|
||||
case Id::Type::Track:
|
||||
data = ServiceProvider<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size);
|
||||
data = Service<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size);
|
||||
break;
|
||||
case Id::Type::Release:
|
||||
data = ServiceProvider<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size);
|
||||
data = Service<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size);
|
||||
break;
|
||||
default:
|
||||
throw BadParameterGenericError {"id"};
|
||||
@@ -1909,7 +1913,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
||||
|
||||
SessionPool::ScopedSession dbSession {_sessionPool};
|
||||
|
||||
switch (ServiceProvider<Auth::IPasswordService>::get()->checkUserPassword(dbSession.get(),
|
||||
switch (Service<Auth::IPasswordService>::get()->checkUserPassword(dbSession.get(),
|
||||
boost::asio::ip::address::from_string(request.clientAddress()),
|
||||
clientInfo.user, clientInfo.password))
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ add_library(lmsutils SHARED
|
||||
impl/String.cpp
|
||||
impl/UUID.cpp
|
||||
impl/WtLogger.cpp
|
||||
impl/Zipper.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsutils INTERFACE
|
||||
|
||||
@@ -26,20 +26,18 @@
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
|
||||
#include <boost/crc.hpp> // for boost::crc_32_type
|
||||
#include <boost/tokenizer.hpp>
|
||||
|
||||
#include "utils/Crc32Calculator.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
void
|
||||
computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
||||
std::uint32_t
|
||||
computeCrc32(const std::filesystem::path& p)
|
||||
{
|
||||
using crc_type = boost::crc_32_type;
|
||||
crc_type result;
|
||||
|
||||
std::ifstream ifs( p.string().c_str(), std::ios_base::binary );
|
||||
Utils::Crc32Calculator crc32;
|
||||
|
||||
std::ifstream ifs {p.string().c_str(), std::ios_base::binary};
|
||||
if (ifs)
|
||||
{
|
||||
do
|
||||
@@ -47,9 +45,9 @@ computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
||||
std::array<char,1024> buffer;
|
||||
|
||||
ifs.read( buffer.data(), buffer.size() );
|
||||
result.process_bytes( buffer.data(), ifs.gcount() );
|
||||
crc32.processBytes( reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount() );
|
||||
}
|
||||
while ( ifs );
|
||||
while (ifs);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,14 +55,7 @@ computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& crc)
|
||||
throw LmsException("Failed to open file '" + p.string() + "'" );
|
||||
}
|
||||
|
||||
|
||||
// Copy the result into a vector of unsigned char
|
||||
const crc_type::value_type checksum = result.checksum();
|
||||
for (std::size_t i = 0; (i+1)*8 <= crc_type::bit_count; i++)
|
||||
{
|
||||
const unsigned char* data = reinterpret_cast<const unsigned char*>( &checksum );
|
||||
crc.push_back(data[i]);
|
||||
}
|
||||
return crc32.getResult();
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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 "utils/Zipper.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
// Done using specs from https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
|
||||
namespace Zip
|
||||
{
|
||||
|
||||
class ZipHeader
|
||||
{
|
||||
public:
|
||||
ZipHeader(std::byte* buffer, std::size_t bufferSize)
|
||||
: _buffer {buffer}
|
||||
, _bufferSize {bufferSize}
|
||||
{}
|
||||
|
||||
enum GeneralPurposeFlag : std::uint16_t
|
||||
{
|
||||
UseDataDescriptor = 1 << 3,
|
||||
LanguageEncoding = 1 << 11,
|
||||
};
|
||||
|
||||
enum CompressionMethod : std::uint16_t
|
||||
{
|
||||
NoCompression = 0,
|
||||
};
|
||||
|
||||
static constexpr std::size_t UnknownCrc32 {0};
|
||||
static constexpr std::size_t UnknownFileSize {0};
|
||||
|
||||
|
||||
protected:
|
||||
void write8(std::size_t offset, std::uint8_t value);
|
||||
void write16(std::size_t offset, std::uint16_t value);
|
||||
void write32(std::size_t offset, std::uint32_t value);
|
||||
void writeDateTime(std::size_t offset, const Wt::WDateTime& time);
|
||||
|
||||
private:
|
||||
std::byte* _buffer {};
|
||||
std::size_t _bufferSize {};
|
||||
};
|
||||
|
||||
void
|
||||
ZipHeader::write8(std::size_t offset, std::uint8_t value)
|
||||
{
|
||||
_buffer[offset] = static_cast<std::byte>(value);
|
||||
}
|
||||
|
||||
void
|
||||
ZipHeader::write16(std::size_t offset, std::uint16_t value)
|
||||
{
|
||||
_buffer[offset] = static_cast<std::byte>(value & 0xff);
|
||||
_buffer[offset + 1] = static_cast<std::byte>(value >> 8);
|
||||
}
|
||||
|
||||
void
|
||||
ZipHeader::write32(std::size_t offset, std::uint32_t value)
|
||||
{
|
||||
_buffer[offset] = static_cast<std::byte>(value & 0xff);
|
||||
_buffer[offset + 1] = static_cast<std::byte>((value >> 8) & 0xff);
|
||||
_buffer[offset + 2] = static_cast<std::byte>((value >> 16) & 0xff);
|
||||
_buffer[offset + 3] = static_cast<std::byte>(value >> 24);
|
||||
}
|
||||
|
||||
void
|
||||
ZipHeader::writeDateTime(std::size_t offset, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
std::uint32_t encodedDateTime{};
|
||||
|
||||
// Date
|
||||
encodedDateTime |= ((dateTime.date().year() - 1980) << 25);
|
||||
encodedDateTime |= (dateTime.date().month() << 21);
|
||||
encodedDateTime |= (dateTime.date().day() << 16);
|
||||
|
||||
// Time
|
||||
encodedDateTime |= (dateTime.time().hour() << 11);
|
||||
encodedDateTime |= (dateTime.time().minute() << 5);
|
||||
encodedDateTime |= (dateTime.time().second() << 1);
|
||||
|
||||
write32(offset, encodedDateTime);
|
||||
}
|
||||
|
||||
class LocalFileHeader : public ZipHeader
|
||||
{
|
||||
public:
|
||||
using ZipHeader::ZipHeader;
|
||||
|
||||
// Setters
|
||||
void setSignature() { write32(0, 0x04034b50); }
|
||||
void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major*10 + minor); }
|
||||
void setGeneralPurposeFlags(std::uint16_t flags) { write16(6, flags); }
|
||||
void setCompressionMethod(CompressionMethod compressionMethod) { write16(8, compressionMethod); }
|
||||
void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(10, dateTime); }
|
||||
void setCrc32UncompressedData(std::uint32_t crc) { write32(14, crc); }
|
||||
void setCompressedSize(std::size_t size) { write32(18, size); }
|
||||
void setUncompressedSize(std::size_t size) { write32(22, size); }
|
||||
void setFileNameLength(std::size_t size) { write16(26, size); }
|
||||
void setExtraFieldLength(std::size_t size) { write16(28, size); }
|
||||
static constexpr std::size_t getHeaderSize() { return 30; }
|
||||
};
|
||||
|
||||
class DataDescriptor : public ZipHeader
|
||||
{
|
||||
public:
|
||||
using ZipHeader::ZipHeader;
|
||||
|
||||
void setSignature() { write32(0, 0x08074b50 ); }
|
||||
void setCrc32UncompressedData(std::uint32_t crc32) { write32(4, crc32); }
|
||||
void setCompressedSize(std::size_t size) { write32(8, size); }
|
||||
void setUncompressedSize(std::size_t size) { write32(12, size); }
|
||||
static constexpr std::size_t getHeaderSize() { return 16; }
|
||||
};
|
||||
|
||||
class CentralDirectoryHeader : public ZipHeader
|
||||
{
|
||||
public:
|
||||
using ZipHeader::ZipHeader;
|
||||
|
||||
void setSignature() { write32(0, 0x02014b50); }
|
||||
void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); }
|
||||
void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + minor); }
|
||||
void setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); }
|
||||
void setCompressionMethod(CompressionMethod method) { write16(10, method); }
|
||||
void setLastModifiedDateTime(const Wt::WDateTime& dateTime) { writeDateTime(12, dateTime); }
|
||||
void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); }
|
||||
void setCompressedSize(std::size_t size) { write32(20, size); }
|
||||
void setUncompressedSize(std::size_t size) { write32(24, size); }
|
||||
void setFileNameLength(std::size_t size) { write16(28, size); }
|
||||
void setExtraFieldLength(std::size_t size) { write16(30, size); }
|
||||
void setFileCommentLength(std::size_t size) { write16(32, size); }
|
||||
void setDiskNumber(std::size_t number) { write16(34, number); }
|
||||
void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); }
|
||||
void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); }
|
||||
void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); }
|
||||
static constexpr std::size_t getHeaderSize() { return 46; }
|
||||
};
|
||||
|
||||
class EndOfCentralDirectoryRecord : public ZipHeader
|
||||
{
|
||||
public:
|
||||
using ZipHeader::ZipHeader;
|
||||
|
||||
void setSignature() { write32(0, 0x06054b50); }
|
||||
void setDiskNumber(unsigned number) { write16(4, number); }
|
||||
void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); }
|
||||
void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); }
|
||||
void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); }
|
||||
void setCentralDirectorySize(std::size_t size) { write32(12, size); }
|
||||
void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); }
|
||||
void setCommentLength(std::size_t length) { write16(20, length); }
|
||||
static constexpr std::size_t getHeaderSize() { return 22; }
|
||||
};
|
||||
|
||||
Zipper::Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime)
|
||||
{
|
||||
for (const auto& [filename, filePath] : files)
|
||||
{
|
||||
FileContext fileContext;
|
||||
fileContext.filePath = filePath;
|
||||
|
||||
std::error_code ec;
|
||||
fileContext.fileSize = std::filesystem::file_size(filePath, ec);
|
||||
if (ec)
|
||||
throw ZipperException {"Cannot get file size for '" + filePath.string() + "': " + ec.message()};
|
||||
|
||||
if (lastModifiedTime.isValid())
|
||||
fileContext.lastModifiedTime = lastModifiedTime;
|
||||
else
|
||||
fileContext.lastModifiedTime = getLastWriteTime(filePath);
|
||||
|
||||
_files[filename] = std::move(fileContext);
|
||||
|
||||
_totalZipSize += LocalFileHeader::getHeaderSize();
|
||||
_totalZipSize += filename.size();
|
||||
if (fileContext.fileSize > 0)
|
||||
{
|
||||
_totalZipSize += fileContext.fileSize;
|
||||
_totalZipSize += DataDescriptor::getHeaderSize();
|
||||
_totalZipSize += CentralDirectoryHeader::getHeaderSize();
|
||||
_totalZipSize += filename.size();
|
||||
}
|
||||
}
|
||||
|
||||
_totalZipSize += EndOfCentralDirectoryRecord::getHeaderSize();
|
||||
if (_totalZipSize > UINT32_MAX)
|
||||
throw ZipperException {"Cannot create a zip file which is larger than " + std::to_string(UINT32_MAX) + " bytes!"};
|
||||
|
||||
_currentFile = std::begin(_files);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeSome(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
// make sure we have some room for the headers
|
||||
assert(bufferSize >= minOutputBufferSize);
|
||||
|
||||
std::size_t nbTotalWrittenBytes {};
|
||||
|
||||
while (!isComplete() && (bufferSize >= minOutputBufferSize))
|
||||
{
|
||||
std::size_t nbWrittenBytes {};
|
||||
|
||||
switch (_writeState)
|
||||
{
|
||||
case WriteState::LocalFileHeader:
|
||||
nbWrittenBytes = writeLocalFileHeader(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::LocalFileHeaderFileName:
|
||||
nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::FileData:
|
||||
nbWrittenBytes = writeFileData(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::DataDescriptor:
|
||||
nbWrittenBytes = writeDataDescriptor(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::CentralDirectoryHeader:
|
||||
nbWrittenBytes = writeCentralDirectoryHeader(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::CentralDirectoryHeaderFileName:
|
||||
nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::EndOfCentralDirectoryRecord:
|
||||
nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize);
|
||||
break;
|
||||
|
||||
case WriteState::Complete:
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += nbWrittenBytes;
|
||||
bufferSize -= nbWrittenBytes;
|
||||
_currentZipOffset += nbWrittenBytes;
|
||||
nbTotalWrittenBytes += nbWrittenBytes ;
|
||||
}
|
||||
|
||||
return nbTotalWrittenBytes;
|
||||
}
|
||||
|
||||
bool
|
||||
Zipper::isComplete() const
|
||||
{
|
||||
return _writeState == WriteState::Complete;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize);
|
||||
|
||||
assert(bufferSize >= minOutputBufferSize);
|
||||
|
||||
if (_currentFile == std::cend(_files))
|
||||
{
|
||||
_currentFile = std::begin(_files);
|
||||
_writeState = WriteState::CentralDirectoryHeader;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LocalFileHeader header {buffer, bufferSize};
|
||||
|
||||
header.setSignature();
|
||||
header.setVersionNeededToExtract(1, 0);
|
||||
header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor);
|
||||
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
|
||||
header.setCrc32UncompressedData(ZipHeader::UnknownCrc32);
|
||||
header.setCompressedSize(ZipHeader::UnknownFileSize);
|
||||
header.setUncompressedSize(ZipHeader::UnknownFileSize);
|
||||
header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime);
|
||||
header.setFileNameLength(_currentFile->first.size());
|
||||
header.setExtraFieldLength(0);
|
||||
|
||||
_writeState = WriteState::LocalFileHeaderFileName;
|
||||
_currentFile->second.localFileHeaderOffset = _currentZipOffset;
|
||||
|
||||
return header.getHeaderSize();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(_currentFile != std::end(_files));
|
||||
|
||||
const std::string& fileName {_currentFile->first};
|
||||
|
||||
assert(_currentOffset <= fileName.size());
|
||||
if (_currentOffset == fileName.size())
|
||||
{
|
||||
_writeState = WriteState::FileData;
|
||||
_currentOffset = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
|
||||
|
||||
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
|
||||
|
||||
_currentOffset += nbBytesToCopy;
|
||||
return nbBytesToCopy;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(_currentFile != std::end(_files));
|
||||
|
||||
if (_currentOffset == _currentFile->second.fileSize)
|
||||
{
|
||||
_currentOffset = 0;
|
||||
_writeState = WriteState::DataDescriptor;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string filePath {_currentFile->second.filePath.string()};
|
||||
|
||||
std::ifstream ifs {filePath.c_str(), std::ios_base::binary};
|
||||
if (!ifs)
|
||||
throw ZipperException {"File '" + filePath + "' does no longer exist!"};
|
||||
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())};
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
|
||||
if (fileSize != _currentFile->second.fileSize)
|
||||
throw ZipperException {"File '" + filePath + "': size mismatch!"};
|
||||
|
||||
const std::size_t nbBytesToRead {std::min(fileSize - _currentOffset, bufferSize)};
|
||||
|
||||
ifs.seekg(_currentOffset, std::ios::beg);
|
||||
ifs.read(reinterpret_cast<char*>(buffer), nbBytesToRead );
|
||||
const ::uint64_t actualReadSize {static_cast<::uint64_t>(ifs.gcount())};
|
||||
|
||||
_currentFile->second.fileCrc32.processBytes(buffer, actualReadSize);
|
||||
_currentOffset += actualReadSize;
|
||||
|
||||
return actualReadSize;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeDataDescriptor(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(bufferSize >= minOutputBufferSize);
|
||||
static_assert(DataDescriptor::getHeaderSize() <= minOutputBufferSize);
|
||||
|
||||
assert(_currentFile != std::end(_files));
|
||||
|
||||
DataDescriptor desc {buffer, bufferSize};
|
||||
desc.setSignature();
|
||||
desc.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult());
|
||||
desc.setCompressedSize(_currentFile->second.fileSize);
|
||||
desc.setUncompressedSize(_currentFile->second.fileSize);
|
||||
|
||||
++_currentFile;
|
||||
_writeState = WriteState::LocalFileHeader;
|
||||
|
||||
return desc.getHeaderSize();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(bufferSize >= minOutputBufferSize);
|
||||
static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize);
|
||||
|
||||
if (_currentFile == std::begin(_files))
|
||||
_centralDirectoryOffset = _currentZipOffset;
|
||||
|
||||
if (_currentFile == std::end(_files))
|
||||
{
|
||||
_writeState = WriteState::EndOfCentralDirectoryRecord;
|
||||
_currentFile = std::begin(_files);
|
||||
return 0;
|
||||
}
|
||||
|
||||
CentralDirectoryHeader header {buffer, bufferSize};
|
||||
header.setSignature();
|
||||
header.setVersionMadeBy(2, 0);
|
||||
header.setVersionNeededToExtract(1, 0);
|
||||
header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor);
|
||||
header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression);
|
||||
header.setCompressedSize(_currentFile->second.fileSize);
|
||||
header.setUncompressedSize(_currentFile->second.fileSize);
|
||||
header.setLastModifiedDateTime(_currentFile->second.lastModifiedTime);
|
||||
header.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult());
|
||||
header.setFileNameLength(_currentFile->first.size());
|
||||
header.setExtraFieldLength(0);
|
||||
header.setFileCommentLength(0);
|
||||
header.setDiskNumber(0);
|
||||
header.setInternalFileAttributes(0);
|
||||
header.setExternalFileAttributes(0);
|
||||
header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset);
|
||||
|
||||
_writeState = WriteState::CentralDirectoryHeaderFileName;
|
||||
_centralDirectorySize += header.getHeaderSize();
|
||||
|
||||
return header.getHeaderSize();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
const std::string& fileName {_currentFile->first};
|
||||
|
||||
assert(_currentOffset <= fileName.size());
|
||||
if (_currentOffset == fileName.size())
|
||||
{
|
||||
_currentOffset = 0;
|
||||
++_currentFile;
|
||||
_writeState = WriteState::CentralDirectoryHeader;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::size_t nbBytesToCopy {std::min<std::size_t>(fileName.size() - _currentOffset, bufferSize)};
|
||||
|
||||
std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast<unsigned char*>(buffer));
|
||||
|
||||
_currentOffset += nbBytesToCopy;
|
||||
_centralDirectorySize += nbBytesToCopy;
|
||||
return nbBytesToCopy;
|
||||
}
|
||||
|
||||
|
||||
std::size_t
|
||||
Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(bufferSize >= minOutputBufferSize);
|
||||
static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize);
|
||||
|
||||
EndOfCentralDirectoryRecord record {buffer, bufferSize};
|
||||
|
||||
record.setSignature();
|
||||
record.setDiskNumber(0);
|
||||
record.setCentralDirectoryDiskNumber(0);
|
||||
record.setNbDiskCentralDirectoryRecords(_files.size());
|
||||
record.setNbCentralDirectoryRecords(_files.size());
|
||||
record.setCentralDirectorySize(_centralDirectorySize);
|
||||
record.setCentralDirectoryOffset(_centralDirectoryOffset);
|
||||
record.setCommentLength(0);
|
||||
|
||||
_writeState = WriteState::Complete;
|
||||
return record.getHeaderSize();
|
||||
}
|
||||
|
||||
} // namespace Zip
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 <boost/crc.hpp> // for boost::crc_32_type
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
|
||||
class Crc32Calculator
|
||||
{
|
||||
public:
|
||||
|
||||
void processBytes(const std::byte* _data, std::size_t dataSize)
|
||||
{
|
||||
_result.process_bytes(_data, dataSize);
|
||||
}
|
||||
|
||||
std::uint32_t getResult() const
|
||||
{
|
||||
return _result.checksum();
|
||||
}
|
||||
|
||||
private:
|
||||
using Crc32Type = boost::crc_32_type;
|
||||
Crc32Type _result;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -82,5 +82,5 @@ class Logger
|
||||
virtual void processLog(const Log& log) = 0;
|
||||
};
|
||||
|
||||
#define LMS_LOG(module, severity) Log(ServiceProvider<Logger>::get(), Module::module, Severity::severity).getOstream()
|
||||
#define LMS_LOG(module, severity) Log(Service<Logger>::get(), Module::module, Severity::severity).getOstream()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
void computeCrc(const std::filesystem::path& p, std::vector<unsigned char>& checksum);
|
||||
std::uint32_t computeCrc32(const std::filesystem::path& p);
|
||||
|
||||
// Make sure the given path is a directory
|
||||
// Create it if needed
|
||||
|
||||
@@ -19,46 +19,44 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
template <typename Class>
|
||||
class ServiceProvider
|
||||
class Service
|
||||
{
|
||||
public:
|
||||
template <class DerivedClass, class ...Args>
|
||||
static
|
||||
Class&
|
||||
create(Args&&... args)
|
||||
Service(std::unique_ptr<Class> service)
|
||||
{
|
||||
static_assert(std::is_base_of<Class, DerivedClass>::value);
|
||||
|
||||
assign(std::make_unique<DerivedClass>(std::forward<Args>(args)...));
|
||||
return *get();
|
||||
assign(std::move(service));
|
||||
}
|
||||
|
||||
template <class ...Args>
|
||||
static
|
||||
Class&
|
||||
create(Args&&... args)
|
||||
~Service()
|
||||
{
|
||||
assign(std::make_unique<Class>(std::forward<Args>(args)...));
|
||||
return *get();
|
||||
clear();
|
||||
}
|
||||
|
||||
static
|
||||
Class&
|
||||
assign(std::unique_ptr<Class> service)
|
||||
{
|
||||
_service = std::move(service);
|
||||
return *get();
|
||||
}
|
||||
Service(const Service&) = delete;
|
||||
Service(Service&&) = delete;
|
||||
Service& operator=(const Service&) = delete;
|
||||
Service& operator=(Service&&) = delete;
|
||||
|
||||
static void clear() { _service.reset(); }
|
||||
Class* operator->() const
|
||||
{
|
||||
return Service<Class>::get();
|
||||
}
|
||||
|
||||
static Class* get() { return _service.get(); }
|
||||
|
||||
private:
|
||||
static Class& assign(std::unique_ptr<Class> service)
|
||||
{
|
||||
assert(!_service);
|
||||
_service = std::move(service);
|
||||
return *get();
|
||||
}
|
||||
static void clear() { _service.reset(); }
|
||||
|
||||
static inline std::unique_ptr<Class> _service;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ template<>
|
||||
std::optional<std::string>
|
||||
readAs(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
replaceInString(const std::string& str, const std::string& from, const std::string& to);
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 <map>
|
||||
#include <set>
|
||||
#include <filesystem>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "Exception.hpp"
|
||||
#include "utils/Crc32Calculator.hpp"
|
||||
|
||||
namespace Zip
|
||||
{
|
||||
|
||||
class ZipperException : public LmsException
|
||||
{
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
// Very simple on-the-fly zip creator, "store" method only
|
||||
class Zipper
|
||||
{
|
||||
public:
|
||||
|
||||
using SizeZype = std::uint64_t;
|
||||
|
||||
Zipper(const std::map<std::string, std::filesystem::path>& files, const Wt::WDateTime& lastModifiedTime = {});
|
||||
|
||||
static constexpr std::size_t minOutputBufferSize = 64;
|
||||
std::size_t writeSome(std::byte* buffer, std::size_t bufferSize);
|
||||
bool isComplete() const;
|
||||
|
||||
SizeZype getTotalZipFile() const { return _totalZipSize; }
|
||||
|
||||
private:
|
||||
void setComplete();
|
||||
|
||||
std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeDataDescriptor(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize);
|
||||
std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize);
|
||||
|
||||
struct FileContext
|
||||
{
|
||||
std::filesystem::path filePath;
|
||||
std::size_t fileSize;
|
||||
Wt::WDateTime lastModifiedTime;
|
||||
Utils::Crc32Calculator fileCrc32;
|
||||
std::size_t localFileHeaderOffset {};
|
||||
};
|
||||
|
||||
using FileContainer = std::map<std::string, FileContext>;
|
||||
FileContainer _files;
|
||||
|
||||
enum class WriteState
|
||||
{
|
||||
LocalFileHeader,
|
||||
LocalFileHeaderFileName,
|
||||
FileData,
|
||||
DataDescriptor,
|
||||
CentralDirectoryHeader,
|
||||
CentralDirectoryHeaderFileName,
|
||||
EndOfCentralDirectoryRecord,
|
||||
Complete,
|
||||
};
|
||||
|
||||
SizeZype _totalZipSize {};
|
||||
WriteState _writeState {WriteState::LocalFileHeader};
|
||||
FileContainer::iterator _currentFile;
|
||||
std::size_t _currentOffset {};
|
||||
std::size_t _currentZipOffset {};
|
||||
std::size_t _centralDirectoryOffset {};
|
||||
std::size_t _centralDirectorySize {};
|
||||
};
|
||||
|
||||
} // namespace Zip
|
||||
|
||||
@@ -28,9 +28,11 @@ add_executable(lms
|
||||
ui/explore/ReleaseView.cpp
|
||||
ui/explore/SearchView.cpp
|
||||
ui/explore/TrackListHelpers.cpp
|
||||
ui/explore/TrackPopup.cpp
|
||||
ui/explore/TracksView.cpp
|
||||
ui/resource/AudioFileResource.cpp
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
ui/resource/DownloadResource.cpp
|
||||
ui/resource/ImageResource.cpp
|
||||
)
|
||||
|
||||
|
||||
+34
-51
@@ -42,31 +42,31 @@ generateWtConfig(std::string execPath)
|
||||
{
|
||||
std::vector<std::string> args;
|
||||
|
||||
const std::filesystem::path wtConfigPath {ServiceProvider<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
|
||||
const std::filesystem::path wtLogFilePath {ServiceProvider<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
|
||||
const std::filesystem::path wtAccessLogFilePath {ServiceProvider<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
|
||||
const std::filesystem::path wtResourcesPath {ServiceProvider<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
|
||||
const std::filesystem::path wtConfigPath {Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
|
||||
const std::filesystem::path wtLogFilePath {Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
|
||||
const std::filesystem::path wtAccessLogFilePath {Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
|
||||
const std::filesystem::path wtResourcesPath {Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
|
||||
|
||||
args.push_back(execPath);
|
||||
args.push_back("--config=" + wtConfigPath.string());
|
||||
args.push_back("--docroot=" + ServiceProvider<IConfig>::get()->getString("docroot"));
|
||||
args.push_back("--approot=" + ServiceProvider<IConfig>::get()->getString("approot"));
|
||||
args.push_back("--deploy-path=" + ServiceProvider<IConfig>::get()->getString("deploy-path", "/"));
|
||||
args.push_back("--docroot=" + Service<IConfig>::get()->getString("docroot"));
|
||||
args.push_back("--approot=" + Service<IConfig>::get()->getString("approot"));
|
||||
args.push_back("--deploy-path=" + Service<IConfig>::get()->getString("deploy-path", "/"));
|
||||
if (!wtResourcesPath.empty())
|
||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||
|
||||
if (ServiceProvider<IConfig>::get()->getBool("tls-enable", false))
|
||||
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
||||
{
|
||||
args.push_back("--https-port=" + std::to_string( ServiceProvider<IConfig>::get()->getULong("listen-port", 5082)));
|
||||
args.push_back("--https-address=" + ServiceProvider<IConfig>::get()->getString("listen-addr", "0.0.0.0"));
|
||||
args.push_back("--ssl-certificate=" + ServiceProvider<IConfig>::get()->getString("tls-cert"));
|
||||
args.push_back("--ssl-private-key=" + ServiceProvider<IConfig>::get()->getString("tls-key"));
|
||||
args.push_back("--ssl-tmp-dh=" + ServiceProvider<IConfig>::get()->getString("tls-dh"));
|
||||
args.push_back("--https-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||
args.push_back("--https-address=" + Service<IConfig>::get()->getString("listen-addr", "0.0.0.0"));
|
||||
args.push_back("--ssl-certificate=" + Service<IConfig>::get()->getString("tls-cert"));
|
||||
args.push_back("--ssl-private-key=" + Service<IConfig>::get()->getString("tls-key"));
|
||||
args.push_back("--ssl-tmp-dh=" + Service<IConfig>::get()->getString("tls-dh"));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.push_back("--http-port=" + std::to_string( ServiceProvider<IConfig>::get()->getULong("listen-port", 5082)));
|
||||
args.push_back("--http-address=" + ServiceProvider<IConfig>::get()->getString("listen-addr", "0.0.0.0"));
|
||||
args.push_back("--http-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||
args.push_back("--http-address=" + Service<IConfig>::get()->getString("listen-addr", "0.0.0.0"));
|
||||
}
|
||||
|
||||
if (!wtAccessLogFilePath.empty())
|
||||
@@ -77,8 +77,8 @@ generateWtConfig(std::string execPath)
|
||||
|
||||
pt.put("server.application-settings.<xmlattr>.location", "*");
|
||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||
pt.put("server.application-settings.log-config", ServiceProvider<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
||||
pt.put("server.application-settings.behind-reverse-proxy", ServiceProvider<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
||||
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||
|
||||
{
|
||||
boost::property_tree::ptree viewport;
|
||||
@@ -132,12 +132,12 @@ int main(int argc, char* argv[])
|
||||
// Make pstream work with ffmpeg
|
||||
close(STDIN_FILENO);
|
||||
|
||||
ServiceProvider<IConfig>::assign(createConfig(configFilePath));
|
||||
ServiceProvider<Logger>::create<WtLogger>();
|
||||
Service<IConfig> config {createConfig(configFilePath)};
|
||||
Service<Logger> logger {std::make_unique<WtLogger>()};
|
||||
|
||||
// Make sure the working directory exists
|
||||
std::filesystem::create_directories(ServiceProvider<IConfig>::get()->getPath("working-dir"));
|
||||
std::filesystem::create_directories(ServiceProvider<IConfig>::get()->getPath("working-dir") / "cache");
|
||||
std::filesystem::create_directories(config->getPath("working-dir"));
|
||||
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
||||
|
||||
// Construct WT configuration and get the argc/argv back
|
||||
std::vector<std::string> wtServerArgs = generateWtConfig(argv[0]);
|
||||
@@ -156,7 +156,7 @@ int main(int argc, char* argv[])
|
||||
Av::Transcoder::init();
|
||||
|
||||
// Initializing a connection pool to the database that will be shared along services
|
||||
Database::Db database {ServiceProvider<IConfig>::get()->getPath("working-dir") / "lms.db"};
|
||||
Database::Db database {config->getPath("working-dir") / "lms.db"};
|
||||
{
|
||||
Database::Session session {database};
|
||||
session.prepareTables();
|
||||
@@ -166,22 +166,22 @@ int main(int argc, char* argv[])
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
// Service initialization order is important
|
||||
ServiceProvider<Auth::IAuthTokenService>::assign(Auth::createAuthTokenService(ServiceProvider<IConfig>::get()->getULong("login-throttler-max-entriees", 10000)));
|
||||
ServiceProvider<Auth::IPasswordService>::assign(Auth::createPasswordService(ServiceProvider<IConfig>::get()->getULong("login-throttler-max-entriees", 10000)));
|
||||
Scanner::IMediaScanner& mediaScanner {ServiceProvider<Scanner::IMediaScanner>::assign(Scanner::createMediaScanner(database))};
|
||||
Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))};
|
||||
Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))};
|
||||
Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0])};
|
||||
coverArtService->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg");
|
||||
Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)};
|
||||
recommendationEngineService->requestLoad();
|
||||
Service<Scanner::IMediaScanner> mediaScannerService {Scanner::createMediaScanner(database)};
|
||||
|
||||
Recommendation::IEngine& recommendationEngine {ServiceProvider<Recommendation::IEngine>::assign(Recommendation::createEngine(database))};
|
||||
CoverArt::IGrabber& coverArtGrabber {ServiceProvider<CoverArt::IGrabber>::assign(CoverArt::createGrabber(argv[0]))};
|
||||
coverArtGrabber.setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg");
|
||||
|
||||
mediaScanner.scanComplete().connect([&]()
|
||||
mediaScannerService->scanComplete().connect([&]()
|
||||
{
|
||||
auto status = mediaScanner.getStatus();
|
||||
auto status = mediaScannerService->getStatus();
|
||||
|
||||
if (status.lastCompleteScanStats->nbChanges() > 0 || status.lastCompleteScanStats->featuresFetched > 0)
|
||||
{
|
||||
LMS_LOG(MAIN, INFO) << "Scanner changed some files, reloading the recommendation engine...";
|
||||
recommendationEngine.requestReload();
|
||||
recommendationEngineService->requestReload();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -189,13 +189,13 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
// Flush cover cache even if no changes:
|
||||
// covers may be external files that changed and we don't keep track of them
|
||||
coverArtGrabber.flushCache();
|
||||
coverArtService->flushCache();
|
||||
});
|
||||
|
||||
API::Subsonic::SubsonicResource subsonicResource {database};
|
||||
|
||||
// bind API resources
|
||||
if (ServiceProvider<IConfig>::get()->getBool("api-subsonic", true))
|
||||
if (config->getBool("api-subsonic", true))
|
||||
server.addResource(&subsonicResource, subsonicResource.getPath());
|
||||
|
||||
// bind UI entry point
|
||||
@@ -203,32 +203,15 @@ int main(int argc, char* argv[])
|
||||
std::bind(UserInterface::LmsApplication::create,
|
||||
std::placeholders::_1, std::ref(database), std::ref(appGroups)));
|
||||
|
||||
// Start
|
||||
LMS_LOG(MAIN, INFO) << "Starting recommendation engine";
|
||||
recommendationEngine.start();
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Starting media scanner...";
|
||||
mediaScanner.start();
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Starting server...";
|
||||
server.start();
|
||||
|
||||
// Wait
|
||||
LMS_LOG(MAIN, INFO) << "Now running...";
|
||||
Wt::WServer::waitForShutdown();
|
||||
|
||||
// Stop
|
||||
LMS_LOG(MAIN, INFO) << "Stopping server...";
|
||||
server.stop();
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Stopping media scanner...";
|
||||
mediaScanner.stop();
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Stopping recommendation engine...";
|
||||
recommendationEngine.stop();
|
||||
|
||||
ServiceProvider<CoverArt::IGrabber>::clear();
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Clean stop!";
|
||||
res = EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
+3
-3
@@ -43,7 +43,7 @@ static
|
||||
void
|
||||
createAuthToken(Database::IdType userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
const std::string secret {ServiceProvider<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)};
|
||||
const std::string secret {Service<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)};
|
||||
|
||||
LmsApp->setCookie(authCookieName,
|
||||
secret,
|
||||
@@ -61,7 +61,7 @@ processAuthToken(const Wt::WEnvironment& env)
|
||||
if (!authCookie)
|
||||
return std::nullopt;
|
||||
|
||||
const auto res {ServiceProvider<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)};
|
||||
const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)};
|
||||
switch (res.state)
|
||||
{
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::NotFound:
|
||||
@@ -124,7 +124,7 @@ class AuthModel : public Wt::WFormModel
|
||||
|
||||
if (field == PasswordField)
|
||||
{
|
||||
switch (ServiceProvider<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
switch (Service<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
LmsApp->getDbSession(),
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
valueText(LoginNameField).toUTF8(),
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "admin/UsersView.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
#include "resource/AudioTranscodeResource.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "Auth.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
@@ -325,7 +326,7 @@ LmsApplication::handleException(LmsApplicationException& e)
|
||||
Wt::WPushButton* btn {t->bindNew<Wt::WPushButton>("btn-go-home", Wt::WString::tr("Lms.Error.go-home"))};
|
||||
btn->clicked().connect([this]()
|
||||
{
|
||||
redirect(".");
|
||||
redirect(defaultPath);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -346,8 +347,9 @@ enum IdxRoot
|
||||
IdxAdminUser,
|
||||
};
|
||||
|
||||
static void
|
||||
handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
static
|
||||
void
|
||||
handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
{
|
||||
static const struct
|
||||
{
|
||||
@@ -371,6 +373,8 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
|
||||
|
||||
LmsApp->doJavaScript(R"($('.navbar-nav li.active').removeClass('active'); $('.navbar-nav a[href="' + location.pathname + '"]').closest('li').addClass('active');)");
|
||||
|
||||
for (const auto& view : views)
|
||||
{
|
||||
if (wApp->internalPathMatches(view.path))
|
||||
@@ -378,7 +382,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
if (view.admin && !isAdmin)
|
||||
break;
|
||||
|
||||
stack->setCurrentIndex(view.index);
|
||||
stack.setCurrentIndex(view.index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -429,9 +433,9 @@ LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth)
|
||||
void
|
||||
LmsApplication::createHome()
|
||||
{
|
||||
_imageResource = std::make_shared<ImageResource>();
|
||||
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
||||
_audioFileResource = std::make_shared<AudioFileResource>();
|
||||
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
||||
_imageResource = std::make_shared<ImageResource>();
|
||||
|
||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||
doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})");
|
||||
@@ -530,7 +534,7 @@ LmsApplication::createHome()
|
||||
{
|
||||
const std::string sessionId {LmsApp->sessionId()};
|
||||
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->scanStarted().connect(this, [=] ()
|
||||
Service<Scanner::IMediaScanner>::get()->scanStarted().connect(this, [=] ()
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
@@ -539,7 +543,7 @@ LmsApplication::createHome()
|
||||
});
|
||||
});
|
||||
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->scanComplete().connect(this, [=] ()
|
||||
Service<Scanner::IMediaScanner>::get()->scanComplete().connect(this, [=] ()
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
@@ -548,7 +552,7 @@ LmsApplication::createHome()
|
||||
});
|
||||
});
|
||||
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats)
|
||||
Service<Scanner::IMediaScanner>::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats)
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
@@ -557,7 +561,7 @@ LmsApplication::createHome()
|
||||
});
|
||||
});
|
||||
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->scheduled().connect(this, [=] (Wt::WDateTime dateTime)
|
||||
Service<Scanner::IMediaScanner>::get()->scheduled().connect(this, [=] (Wt::WDateTime dateTime)
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
@@ -572,7 +576,7 @@ LmsApplication::createHome()
|
||||
{
|
||||
if (isUserAdmin())
|
||||
{
|
||||
const auto& stats {*ServiceProvider<Scanner::IMediaScanner>::get()->getStatus().lastCompleteScanStats};
|
||||
const auto& stats {*Service<Scanner::IMediaScanner>::get()->getStatus().lastCompleteScanStats};
|
||||
|
||||
notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
||||
.arg(static_cast<unsigned>(stats.nbFiles()))
|
||||
@@ -596,10 +600,10 @@ LmsApplication::createHome()
|
||||
|
||||
internalPathChanged().connect([=]
|
||||
{
|
||||
handlePathChange(mainStack, isUserAdmin());
|
||||
handlePathChange(*mainStack, isUserAdmin());
|
||||
});
|
||||
|
||||
handlePathChange(mainStack, isUserAdmin());
|
||||
handlePathChange(*mainStack, isUserAdmin());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -278,7 +278,7 @@ PlayQueue::loadTrack(std::size_t pos, bool play)
|
||||
}
|
||||
|
||||
if (addRadioTrack)
|
||||
enqueueRadioTrack();
|
||||
enqueueRadioTracks();
|
||||
|
||||
updateCurrentTrack(true);
|
||||
|
||||
@@ -498,9 +498,9 @@ PlayQueue::addSome()
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::enqueueRadioTrack()
|
||||
PlayQueue::enqueueRadioTracks()
|
||||
{
|
||||
const std::vector<Database::IdType> trackToAddIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 1)};
|
||||
const std::vector<Database::IdType> trackToAddIds {Service<Recommendation::IEngine>::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 3)};
|
||||
enqueueTracks(trackToAddIds);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class PlayQueue : public Wt::WTemplate
|
||||
void clearTracks();
|
||||
std::size_t enqueueTracks(const std::vector<Database::IdType>& trackIds);
|
||||
void addSome();
|
||||
void enqueueRadioTrack();
|
||||
void enqueueRadioTracks();
|
||||
void updateInfo();
|
||||
void updateCurrentTrack(bool selected);
|
||||
void updateRepeatBtn();
|
||||
|
||||
@@ -120,7 +120,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
User::PasswordHash passwordHash;
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
passwordHash = ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
@@ -243,7 +243,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
if (!valueText(PasswordOldField).empty())
|
||||
{
|
||||
switch (ServiceProvider<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
switch (Service<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
LmsApp->getDbSession(),
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
LmsApp->getUserLoginName(),
|
||||
@@ -271,7 +271,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8()))
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
else
|
||||
@@ -468,7 +468,7 @@ SettingsView::refreshView()
|
||||
|
||||
// Subsonic
|
||||
{
|
||||
t->setCondition("if-has-subsonic-api", ServiceProvider<IConfig>::get()->getBool("api-subsonic", true));
|
||||
t->setCondition("if-has-subsonic-api", Service<IConfig>::get()->getBool("api-subsonic", true));
|
||||
|
||||
// Transcode
|
||||
auto transcode {std::make_unique<Wt::WCheckBox>()};
|
||||
|
||||
@@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView()
|
||||
{
|
||||
model->saveData();
|
||||
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->requestReload();
|
||||
Service<Scanner::IMediaScanner>::get()->requestReload();
|
||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView()
|
||||
|
||||
immScanBtn->clicked().connect([=] ()
|
||||
{
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
t->updateView(model.get());
|
||||
|
||||
@@ -62,7 +62,7 @@ class InitWizardModel : public Wt::WFormModel
|
||||
|
||||
void saveData()
|
||||
{
|
||||
const Database::User::PasswordHash passwordHash {ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())};
|
||||
const Database::User::PasswordHash passwordHash {Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())};
|
||||
|
||||
auto transaction(LmsApp->getDbSession().createUniqueTransaction());
|
||||
|
||||
@@ -97,7 +97,7 @@ class InitWizardModel : public Wt::WFormModel
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8()))
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -146,20 +146,20 @@ ScannerController::refreshContents()
|
||||
actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now"));
|
||||
actionBtn->actionButton()->clicked().connect([]
|
||||
{
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
auto popup = std::make_unique<Wt::WPopupMenu>();
|
||||
popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"));
|
||||
popup->itemSelected().connect([]
|
||||
{
|
||||
ServiceProvider<Scanner::IMediaScanner>::get()->requestImmediateScan(true);
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(true);
|
||||
});
|
||||
actionBtn->dropDownButton()->setMenu(std::move(popup));
|
||||
actionBtn->dropDownButton()->addStyleClass("btn-primary");
|
||||
|
||||
|
||||
const IMediaScanner::Status status {ServiceProvider<IMediaScanner>::get()->getStatus()};
|
||||
const IMediaScanner::Status status {Service<IMediaScanner>::get()->getStatus()};
|
||||
if (status.lastCompleteScanStats)
|
||||
{
|
||||
bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
|
||||
|
||||
@@ -80,7 +80,7 @@ class UserModel : public Wt::WFormModel
|
||||
{
|
||||
std::optional<Database::User::PasswordHash> passwordHash;
|
||||
if (!valueText(PasswordField).empty())
|
||||
passwordHash = ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
@@ -143,7 +143,7 @@ class UserModel : public Wt::WFormModel
|
||||
else
|
||||
{
|
||||
// Evaluate the strength of the password for non demo accounts
|
||||
if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8()))
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ UserView::refreshView()
|
||||
|
||||
// Demo account
|
||||
t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>());
|
||||
if (!userId && ServiceProvider<IConfig>::get()->getBool("demo", false))
|
||||
if (!userId && Service<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"));
|
||||
|
||||
@@ -30,9 +30,9 @@ createAuthModeModel()
|
||||
{
|
||||
auto model {std::make_unique<AuthModeModel>()};
|
||||
|
||||
if (ServiceProvider<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::Internal))
|
||||
if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::Internal))
|
||||
model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.internal"), Database::User::AuthMode::Internal);
|
||||
if (ServiceProvider<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::PAM))
|
||||
if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::PAM))
|
||||
model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.pam"), Database::User::AuthMode::PAM);
|
||||
|
||||
return model;
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -71,9 +72,9 @@ Artist::refreshView()
|
||||
|
||||
const auto artistId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/artist/"))};
|
||||
if (!artistId)
|
||||
return;
|
||||
throw ArtistNotFoundException {*artistId};
|
||||
|
||||
const std::vector<Database::IdType> similarArtistIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||
const std::vector<Database::IdType> similarArtistIds {Service<Recommendation::IEngine>::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
@@ -132,6 +133,30 @@ Artist::refreshView()
|
||||
artistsAction.emit(PlayQueueAction::PlayLast, {*artistId});
|
||||
});
|
||||
|
||||
bool isStarred {};
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
if (auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)})
|
||||
isStarred = LmsApp->getUser()->hasStarredArtist(artist);
|
||||
}
|
||||
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
|
||||
->triggered().connect(this, [=]
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
auto artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)};
|
||||
if (!artist)
|
||||
return;
|
||||
|
||||
if (isStarred)
|
||||
LmsApp->getUser().modify()->unstarArtist(artist);
|
||||
else
|
||||
LmsApp->getUser().modify()->starArtist(artist);
|
||||
});
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(*artistId)});
|
||||
|
||||
popup->exec(moreBtn);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ Artists::Artists(Filters* filters)
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
@@ -167,6 +168,15 @@ Artists::getArtists(std::optional<Range> range, bool& moreResults)
|
||||
artists = getRandomArtists(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::Starred:
|
||||
artists = Artist::getStarred(LmsApp->getDbSession(),
|
||||
LmsApp->getUser(),
|
||||
_filters->getClusterIds(),
|
||||
linkType,
|
||||
Artist::SortMethod::BySortName,
|
||||
range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
artists = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())
|
||||
->getArtistsReverse(_filters->getClusterIds(),
|
||||
|
||||
@@ -46,6 +46,7 @@ class Artists : public Wt::WTemplate
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
Starred,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "ReleasePopup.hpp"
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
@@ -42,6 +45,31 @@ namespace UserInterface
|
||||
releasesAction.emit(PlayQueueAction::PlayLast, {releaseId});
|
||||
});
|
||||
|
||||
bool isStarred {};
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
if (auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)})
|
||||
isStarred = LmsApp->getUser()->hasStarredRelease(release);
|
||||
}
|
||||
|
||||
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
|
||||
->triggered().connect(&target, [=]
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
auto release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
|
||||
if (!release)
|
||||
return;
|
||||
|
||||
if (isStarred)
|
||||
LmsApp->getUser().modify()->unstarRelease(release);
|
||||
else
|
||||
LmsApp->getUser().modify()->starRelease(release);
|
||||
});
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(releaseId)});
|
||||
|
||||
popup->popup(&target);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -39,6 +40,7 @@
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "ReleasePopup.hpp"
|
||||
#include "TrackPopup.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -74,9 +76,9 @@ Release::refreshView()
|
||||
|
||||
const auto releaseId {StringUtils::readAs<Database::IdType>(wApp->internalPathNextPart("/release/"))};
|
||||
if (!releaseId)
|
||||
return;
|
||||
throw ReleaseNotFoundException {*releaseId};
|
||||
|
||||
const std::vector<Database::IdType> similarReleasesIds {ServiceProvider<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)};
|
||||
const std::vector<Database::IdType> similarReleasesIds {Service<Recommendation::IEngine>::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
@@ -240,15 +242,7 @@ Release::refreshView()
|
||||
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
|
||||
moreBtn->clicked().connect([=]()
|
||||
{
|
||||
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
|
||||
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
|
||||
->triggered().connect(moreBtn, [=]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
||||
});
|
||||
|
||||
popup->popup(moreBtn);
|
||||
displayTrackPopupMenu(*moreBtn, trackId, tracksAction);
|
||||
});
|
||||
|
||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
@@ -61,6 +61,7 @@ _filters {filters}
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
@@ -198,6 +199,10 @@ Releases::getReleases(std::optional<Range> range, bool& moreResults)
|
||||
releases = getRandomReleases(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::Starred:
|
||||
releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
releases = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getReleasesReverse(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
@@ -48,6 +48,7 @@ class Releases : public Wt::WTemplate
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
Starred,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackPopup.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -93,18 +95,10 @@ namespace UserInterface::TrackListHelpers
|
||||
tracksAction.emit(PlayQueueAction::Play, {trackId});
|
||||
});
|
||||
|
||||
Wt::WText* moreBtn = entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.Explore.template.more-btn"), Wt::TextFormat::XHTML)};
|
||||
moreBtn->clicked().connect([=, &tracksAction]
|
||||
{
|
||||
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
|
||||
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
|
||||
->triggered().connect(moreBtn, [=, &tracksAction]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
||||
});
|
||||
|
||||
popup->popup(moreBtn);
|
||||
displayTrackPopupMenu(*moreBtn, trackId, tracksAction);
|
||||
});
|
||||
|
||||
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::IdType loadedTrackId)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 "TrackPopup.hpp"
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
void
|
||||
displayTrackPopupMenu(Wt::WInteractWidget& target,
|
||||
Database::IdType trackId,
|
||||
PlayQueueActionSignal& tracksAction)
|
||||
{
|
||||
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
|
||||
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.play-last"))
|
||||
->triggered().connect(&target, [=, &tracksAction]
|
||||
{
|
||||
tracksAction.emit(PlayQueueAction::PlayLast, {trackId});
|
||||
});
|
||||
|
||||
bool isStarred {};
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
if (auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)})
|
||||
isStarred = LmsApp->getUser()->hasStarredTrack(track);
|
||||
}
|
||||
popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star"))
|
||||
->triggered().connect(&target, [=]
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
if (isStarred)
|
||||
LmsApp->getUser().modify()->unstarTrack(track);
|
||||
else
|
||||
LmsApp->getUser().modify()->starTrack(track);
|
||||
});
|
||||
popup->addItem(Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
|
||||
|
||||
popup->popup(&target);
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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/WWidget.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "PlayQueueAction.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
void displayTrackPopupMenu(Wt::WInteractWidget& target,
|
||||
Database::IdType releaseId,
|
||||
PlayQueueActionSignal& releasesAction);
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -63,6 +63,7 @@ _filters {filters}
|
||||
};
|
||||
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.random"), Mode::Random);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.starred"), Mode::Starred);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-played"), Mode::RecentlyPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.most-played"), Mode::MostPlayed);
|
||||
addItem(*menu, Wt::WString::tr("Lms.Explore.recently-added"), Mode::RecentlyAdded);
|
||||
@@ -185,6 +186,10 @@ Tracks::getTracks(std::optional<Range> range, bool& moreResults)
|
||||
tracks = getRandomTracks(range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::Starred:
|
||||
tracks = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), _filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
case Mode::RecentlyPlayed:
|
||||
tracks = LmsApp->getUser()->getPlayedTrackList(LmsApp->getDbSession())->getTracksReverse(_filters->getClusterIds(), range, moreResults);
|
||||
break;
|
||||
|
||||
@@ -47,6 +47,7 @@ class Tracks : public Wt::WTemplate
|
||||
enum class Mode
|
||||
{
|
||||
Random,
|
||||
Starred,
|
||||
RecentlyPlayed,
|
||||
RecentlyAdded,
|
||||
MostPlayed,
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 "DownloadResource.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <Wt/Http/Response.h>
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Zipper.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
#define LOG(level) LMS_LOG(UI, level) << "Download resource: "
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
DownloadResource::~DownloadResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
void
|
||||
DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::shared_ptr<Zip::Zipper> zipper;
|
||||
|
||||
// First, see if this request is for a continuation
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
if (continuation)
|
||||
zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::Zipper>>(continuation->data());
|
||||
else
|
||||
{
|
||||
zipper = createZipper();
|
||||
response.setMimeType("application/zip");
|
||||
}
|
||||
|
||||
if (!zipper)
|
||||
return;
|
||||
|
||||
std::array<std::byte, bufferSize> buffer;
|
||||
std::size_t nbWrittenBytes {zipper->writeSome(buffer.data(), buffer.size())};
|
||||
|
||||
response.out().write(reinterpret_cast<const char *>(buffer.data()), nbWrittenBytes);
|
||||
|
||||
if (!zipper->isComplete())
|
||||
{
|
||||
auto* continuation {response.createContinuation()};
|
||||
continuation->setData(zipper);
|
||||
}
|
||||
}
|
||||
catch (Zip::ZipperException& exception)
|
||||
{
|
||||
LOG(ERROR) << "Zipper exception: " << exception.what();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
std::string
|
||||
getArtistPathName(Database::Artist::pointer artist)
|
||||
{
|
||||
return StringUtils::replaceInString(artist->getName(), "/", "_");
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
getReleaseArtistPathName(Database::Release::pointer release)
|
||||
{
|
||||
std::string releaseArtistName;
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Database::Artist>> artists;
|
||||
|
||||
artists = release->getReleaseArtists();
|
||||
if (artists.empty())
|
||||
artists = release->getArtists();
|
||||
|
||||
if (artists.size() > 1)
|
||||
releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
|
||||
else if (artists.size() == 1)
|
||||
releaseArtistName = artists.front()->getName();
|
||||
|
||||
releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_");
|
||||
|
||||
return releaseArtistName;
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
getReleasePathName(Database::Release::pointer release)
|
||||
{
|
||||
std::string releaseName;
|
||||
|
||||
if (auto releaseYear {release->getReleaseYear()})
|
||||
releaseName += std::to_string(*releaseYear) + " - ";
|
||||
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
||||
|
||||
return releaseName;
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
getTrackPathName(Database::Track::pointer track)
|
||||
{
|
||||
std::ostringstream fileName;
|
||||
|
||||
if (auto discNumber {track->getDiscNumber()})
|
||||
fileName << *discNumber << ".";
|
||||
if (auto trackNumber {track->getTrackNumber()})
|
||||
fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - ";
|
||||
|
||||
fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string();
|
||||
|
||||
return fileName.str();
|
||||
}
|
||||
|
||||
static
|
||||
std::unique_ptr<Zip::Zipper>
|
||||
createZipper(const std::vector<Database::Track::pointer>& tracks)
|
||||
{
|
||||
std::map<std::string, std::filesystem::path> files;
|
||||
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
{
|
||||
std::string releaseName;
|
||||
std::string releaseArtistName;
|
||||
if (auto release {track->getRelease()})
|
||||
{
|
||||
releaseName = getReleasePathName(release);
|
||||
releaseArtistName = getReleaseArtistPathName(release);
|
||||
}
|
||||
|
||||
std::string fileName;
|
||||
if (!releaseArtistName.empty())
|
||||
fileName += releaseArtistName + "/";
|
||||
if (!releaseName.empty())
|
||||
fileName += releaseName + "/";
|
||||
fileName += getTrackPathName(track);
|
||||
|
||||
files.emplace(fileName, track->getPath());
|
||||
}
|
||||
|
||||
return std::make_unique<Zip::Zipper>(files, Wt::WLocalDateTime::currentDateTime().toUTC());
|
||||
}
|
||||
|
||||
DownloadArtistResource::DownloadArtistResource(Database::IdType artistId)
|
||||
: _artistId {artistId}
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)};
|
||||
if (artist)
|
||||
suggestFileName(getArtistPathName(artist) + ".zip");
|
||||
}
|
||||
|
||||
std::unique_ptr<Zip::Zipper>
|
||||
DownloadArtistResource::createZipper()
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)};
|
||||
if (!artist)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot find artist";
|
||||
return {};
|
||||
}
|
||||
|
||||
return UserInterface::createZipper(artist->getTracks());
|
||||
}
|
||||
|
||||
DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId)
|
||||
: _releaseId {releaseId}
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
|
||||
if (release)
|
||||
suggestFileName(getReleasePathName(release) + ".zip");
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Zip::Zipper>
|
||||
DownloadReleaseResource::createZipper()
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)};
|
||||
if (!release)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot find release";
|
||||
return {};
|
||||
}
|
||||
|
||||
return UserInterface::createZipper(release->getTracks());
|
||||
}
|
||||
|
||||
DownloadTrackResource::DownloadTrackResource(Database::IdType trackId)
|
||||
: _trackId {trackId}
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
|
||||
if (track)
|
||||
suggestFileName(getTrackPathName(track) + ".zip");
|
||||
}
|
||||
|
||||
std::unique_ptr<Zip::Zipper>
|
||||
DownloadTrackResource::createZipper()
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), _trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot find track";
|
||||
return {};
|
||||
}
|
||||
|
||||
return UserInterface::createZipper({track});
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 <memory>
|
||||
#include <Wt/WResource.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/Zipper.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class DownloadResource : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t bufferSize {32768};
|
||||
|
||||
~DownloadResource();
|
||||
|
||||
private:
|
||||
|
||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
virtual std::unique_ptr<Zip::Zipper> createZipper() = 0;
|
||||
};
|
||||
|
||||
class DownloadArtistResource : public DownloadResource
|
||||
{
|
||||
public:
|
||||
DownloadArtistResource(Database::IdType artistId);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Zip::Zipper> createZipper() override;
|
||||
Database::IdType _artistId;
|
||||
};
|
||||
|
||||
class DownloadReleaseResource : public DownloadResource
|
||||
{
|
||||
public:
|
||||
DownloadReleaseResource(Database::IdType releaseId);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Zip::Zipper> createZipper() override;
|
||||
Database::IdType _releaseId;
|
||||
};
|
||||
|
||||
class DownloadTrackResource : public DownloadResource
|
||||
{
|
||||
public:
|
||||
DownloadTrackResource(Database::IdType trackId);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Zip::Zipper> createZipper() override;
|
||||
Database::IdType _trackId;
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -90,7 +90,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
||||
// DbSession are not thread safe
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock {LmsApp};
|
||||
cover = ServiceProvider<CoverArt::IGrabber>::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size);
|
||||
cover = Service<CoverArt::IGrabber>::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size);
|
||||
}
|
||||
}
|
||||
else if (releaseIdStr)
|
||||
@@ -104,7 +104,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
||||
// DbSession are not thread safe
|
||||
{
|
||||
Wt::WApplication::UpdateLock lock {LmsApp};
|
||||
cover = ServiceProvider<CoverArt::IGrabber>::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size);
|
||||
cover = Service<CoverArt::IGrabber>::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -47,7 +47,8 @@ class ImageResource : public Wt::WResource
|
||||
|
||||
static std::string getMimeType();
|
||||
|
||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||
private:
|
||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1248,22 +1248,26 @@ void
|
||||
testSingleStarredArtist(Session& session)
|
||||
{
|
||||
ScopedArtist artist {session, "MyArtist"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLink::Type::Artist)};
|
||||
user.get().modify()->starArtist(artist.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {user->getStarredArtists()};
|
||||
CHECK(user->hasStarredArtist(artist.get()));
|
||||
|
||||
bool hasMore {};
|
||||
auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
CHECK(user->hasStarredArtist(artist.get()));
|
||||
CHECK(hasMore == false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,22 +1276,26 @@ void
|
||||
testSingleStarredRelease(Session& session)
|
||||
{
|
||||
ScopedRelease release {session, "MyRelease"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
user.get().modify()->starRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto releases {user->getStarredReleases()};
|
||||
CHECK(user->hasStarredRelease(release.get()));
|
||||
|
||||
bool hasMore {};
|
||||
auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
|
||||
CHECK(releases.size() == 1);
|
||||
CHECK(releases.front().id() == release.getId());
|
||||
|
||||
CHECK(user->hasStarredRelease(release.get()));
|
||||
CHECK(hasMore == false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,11 +1315,13 @@ testSingleStarredTrack(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto tracks {user->getStarredTracks()};
|
||||
CHECK(user->hasStarredTrack(track.get()));
|
||||
|
||||
bool hasMore {};
|
||||
auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)};
|
||||
CHECK(tracks.size() == 1);
|
||||
CHECK(tracks.front().id() == track.getId());
|
||||
|
||||
CHECK(user->hasStarredTrack(track.get()));
|
||||
CHECK(hasMore == false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1882,7 +1892,7 @@ int main()
|
||||
try
|
||||
{
|
||||
// log to stdout
|
||||
ServiceProvider<Logger>::create<StreamLogger>(std::cout);
|
||||
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout)};
|
||||
|
||||
const std::filesystem::path tmpFile {std::tmpnam(nullptr)};
|
||||
ScopedFileDeleter tmpFileDeleter {tmpFile};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
add_subdirectory(metadata)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(zipper)
|
||||
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ int main(int argc, char *argv[])
|
||||
try
|
||||
{
|
||||
// log to stdout
|
||||
ServiceProvider<Logger>::create<StreamLogger>(std::cout);
|
||||
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout)};
|
||||
|
||||
for (std::size_t i {}; i < static_cast<std::size_t>(argc - 1); ++i)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ add_executable(lms-recommendation
|
||||
target_link_libraries(lms-recommendation PRIVATE
|
||||
lmsdatabase
|
||||
lmsrecommendation
|
||||
Boost::program_options
|
||||
)
|
||||
|
||||
install(TARGETS lms-recommendation DESTINATION bin)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <stdexcept>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
@@ -130,16 +132,32 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
try
|
||||
{
|
||||
namespace po = boost::program_options;
|
||||
|
||||
// log to stdout
|
||||
ServiceProvider<Logger>::create<StreamLogger>(std::cout);
|
||||
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout)};
|
||||
|
||||
std::filesystem::path configFilePath {"/etc/lms.conf"};
|
||||
if (argc >= 2)
|
||||
configFilePath = std::string(argv[1], 0, 256);
|
||||
po::options_description desc{"Allowed options"};
|
||||
desc.add_options()
|
||||
("help,h", "print usage message")
|
||||
("conf,c", po::value<std::string>()->default_value("/etc/lms.conf"), "LMS config file")
|
||||
("artists,a", "Display recommendation for artists")
|
||||
("releases,r", "Display recommendation for releases")
|
||||
("tracks,t", "Display recommendation for tracks")
|
||||
;
|
||||
|
||||
ServiceProvider<IConfig>::assign(createConfig(configFilePath));
|
||||
po::variables_map vm;
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
|
||||
Database::Db db {ServiceProvider<IConfig>::get()->getPath("working-dir") / "lms.db"};
|
||||
if (vm.count("help"))
|
||||
{
|
||||
std::cout << desc << std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Service<IConfig> config {createConfig(vm["conf"].as<std::string>())};
|
||||
|
||||
Database::Db db {config->getPath("working-dir") / "lms.db"};
|
||||
Database::Session session {db};
|
||||
|
||||
std::cout << "Creating recommendation engine..." << std::endl;
|
||||
@@ -153,17 +171,20 @@ int main(int argc, char *argv[])
|
||||
sem.notify();
|
||||
});
|
||||
|
||||
engine->start();
|
||||
engine->requestLoad();
|
||||
|
||||
std::cout << "Waiting for the recommendation engine to be loaded..." << std::endl;
|
||||
sem.wait();
|
||||
std::cout << "Recommendation engine loaded!" << std::endl;
|
||||
|
||||
dumpTracksRecommendation(db, *engine);
|
||||
dumpReleasesRecommendation(db, *engine);
|
||||
dumpArtistsRecommendation(db, *engine);
|
||||
if (vm.count("tracks"))
|
||||
dumpTracksRecommendation(db, *engine);
|
||||
|
||||
engine->stop();
|
||||
if (vm.count("releases"))
|
||||
dumpReleasesRecommendation(db, *engine);
|
||||
|
||||
if (vm.count("artists"))
|
||||
dumpArtistsRecommendation(db, *engine);
|
||||
}
|
||||
catch( std::exception& e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
add_executable(lms-zipper
|
||||
LmsZipper.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(lms-zipper PRIVATE
|
||||
lmsutils
|
||||
)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 <unistd.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/StreamLogger.hpp"
|
||||
#include "utils/Zipper.hpp"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// log to stdout
|
||||
Service<Logger> logger {std::make_unique<StreamLogger>(std::cout)};
|
||||
|
||||
if (argc < 2)
|
||||
{
|
||||
std::cerr << "Usage: <archive> <file> [...]" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::filesystem::path zipPath {argv[1]};
|
||||
|
||||
std::map<std::string, std::filesystem::path> files;
|
||||
for (int i {2}; i < argc; ++i)
|
||||
{
|
||||
std::filesystem::path path {argv[i]};
|
||||
files.emplace(path.relative_path(), path);
|
||||
}
|
||||
|
||||
|
||||
std::cout << "Compressing " << files.size() << " files..." << std::endl;
|
||||
|
||||
using namespace Zip;
|
||||
|
||||
std::ofstream ofs {zipPath.string().c_str(), std::ios_base::binary};
|
||||
if (!ofs)
|
||||
{
|
||||
std::cerr << "Cannot open file '" << zipPath.string() << "' for writing";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Zipper zipper {files};
|
||||
|
||||
while (!zipper.isComplete())
|
||||
{
|
||||
//std::array<std::byte, Zipper::minOutputBufferSize> buffer;
|
||||
std::array<std::byte, 65536> buffer;
|
||||
|
||||
std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())};
|
||||
ofs.write(reinterpret_cast<const char*>(buffer.data()), nbWrittenBytes);
|
||||
}
|
||||
|
||||
std::cout << "Total zip size = " << zipper.getTotalZipFile() << std::endl;
|
||||
}
|
||||
catch (const ZipperException& e)
|
||||
{
|
||||
std::cerr << "Caught Zipper exception: " << e.what() << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user