Merge branch 'develop' for release v3.23.0
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
*.sw?
|
||||
*.*~
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
build/
|
||||
|
||||
@@ -14,7 +14,6 @@ extraction:
|
||||
- libavutil-dev
|
||||
- libavformat-dev
|
||||
- libstb-dev
|
||||
- libpstreams-dev
|
||||
- libtag1-dev
|
||||
- libpam0g-dev
|
||||
after_prepare:
|
||||
|
||||
@@ -16,7 +16,6 @@ addons:
|
||||
- libavformat-dev
|
||||
- ffmpeg
|
||||
- libstb-dev
|
||||
- libpstreams-dev
|
||||
- libtag1-dev
|
||||
- libpam0g-dev
|
||||
- libgraphicsmagick++1-dev
|
||||
|
||||
+6
-6
@@ -8,16 +8,16 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
include(CTest)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(FFMPEGAV REQUIRED)
|
||||
find_package(Taglib REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options)
|
||||
find_package(PStreams REQUIRED)
|
||||
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
pkg_check_modules(GRAPHICSMAGICKXX GraphicsMagick++)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(GraphicsMagick++ IMPORTED_TARGET GraphicsMagick++)
|
||||
|
||||
# WT
|
||||
if (NOT Wt_FOUND)
|
||||
@@ -51,13 +51,13 @@ endif ()
|
||||
# IMAGE
|
||||
if (STB_FOUND)
|
||||
set(IMAGE_LIBRARY STB CACHE STRING "STB library")
|
||||
elseif (GRAPHICSMAGICKXX_FOUND)
|
||||
elseif (GraphicsMagick++_FOUND)
|
||||
set(IMAGE_LIBRARY GraphicsMagick++ CACHE STRING "GraphicsMagick++")
|
||||
else ()
|
||||
message(FATAL_ERROR "Cannot find suitable image library")
|
||||
endif()
|
||||
set_property(CACHE IMAGE_LIBRARY PROPERTY STRINGS GraphicsMagick++ STB)
|
||||
if (IMAGE_LIBRARY STREQUAL GraphicsMagick++ AND NOT GRAPHICSMAGICKXX_FOUND)
|
||||
if (IMAGE_LIBRARY STREQUAL GraphicsMagick++ AND NOT GraphicsMagick++_FOUND)
|
||||
message(FATAL_ERROR "GraphicsMagick++ not found")
|
||||
elseif (IMAGE_LIBRARY STREQUAL STB AND NOT STB_FOUND)
|
||||
message(FATAL_ERROR "STB not found")
|
||||
|
||||
+4
-1
@@ -43,7 +43,7 @@ __Notes__:
|
||||
* a C++17 compiler is needed
|
||||
* ffmpeg version 4 minimum is required
|
||||
```sh
|
||||
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev
|
||||
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev
|
||||
```
|
||||
__Notes__:
|
||||
* libpam0g-dev is optional (only for using PAM authentication)
|
||||
@@ -157,6 +157,9 @@ server {
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_read_timeout 10m;
|
||||
proxy_send_timeout 10m;
|
||||
keepalive_timeout 10m;
|
||||
|
||||
location / {
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<div class="Lms-header">
|
||||
<h2><div class="Lms-artistname text-primary">${name}</div></h2>
|
||||
${clusters}
|
||||
${play-btn}${more-btn}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="row">
|
||||
${releases class="Lms-explore-artist-entries-container"}
|
||||
|
||||
+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-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.more-btn"><i class="fa fa-fw fa-plus-circle"></i></message>
|
||||
<message id="Lms.Explore.template.play-btn"><i class="fa fa-fw fa-play-circle"></i></message>
|
||||
|
||||
<message id="Lms.Explore.template.filters">
|
||||
${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"}
|
||||
|
||||
@@ -15,26 +15,26 @@
|
||||
</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-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 class="col-xs-5 col-sm-3 col-md-2 Lms-player-controls">
|
||||
<i id="lms-mp-previous" class="fa fa-fw fa-step-backward Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-playpause" class="fa fa-fw fa-play Lms-player-btn Lms-btn Lms-player-btn-controls"></i><i id="lms-mp-next" class="fa fa-fw fa-step-forward Lms-player-btn Lms-btn Lms-player-btn-controls"></i>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2">
|
||||
<div class="hidden-xs hidden-sm col-md-2">
|
||||
<div class="Lms-player-volume">
|
||||
<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">
|
||||
<span class="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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-7 col-sm-6">
|
||||
<div class="col-xs-7 col-sm-7 col-md-6">
|
||||
<div class="Lms-player-text-center">
|
||||
${title class="Lms-player-text Lms-trackname text-primary"}
|
||||
</div>
|
||||
<div class="Lms-player-text-center">
|
||||
<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>
|
||||
${release class="Lms-btn Lms-player-text Lms-releasename text-primary"}<span class="hidden-xs"><small>${artist class="Lms-btn Lms-player-text Lms-artistname text-muted"}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2 Lms-player-duration text-muted">
|
||||
<div class="hidden-xs col-sm-2 col-md-2 Lms-player-duration text-muted">
|
||||
<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>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">Daily</message>
|
||||
<message id="Lms.Admin.Database.database">Music collection</message>
|
||||
<message id="Lms.Admin.Database.hourly">Hourly</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
|
||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Music collection</message>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">Tous les jours</message>
|
||||
<message id="Lms.Admin.Database.database">Collection de musiques</message>
|
||||
<message id="Lms.Admin.Database.hourly">Toutes les heures</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
|
||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||
<message id="Lms.Admin.Database.menu-database"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Collection de musiques</message>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
<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-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.clear-btn"><i class="fa fa-fw fa-times-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.shuffle-btn"><i class="fa fa-fw fa-random"></i></message>
|
||||
<message id="Lms.PlayQueue.template.repeat-btn"><i class="fa fa-fw fa-repeat"></i></message>
|
||||
<message id="Lms.PlayQueue.template.radio-btn"><i class="fa fa-fw fa-rss"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="Lms-header">
|
||||
${clear-btn}${shuffle-btn}${repeat-btn}${radio-btn}
|
||||
${clear-btn class="Lms-playqueue-btn Lms-btn"}${shuffle-btn class="Lms-playqueue-btn Lms-btn"}${repeat-btn class="Lms-playqueue-btn Lms-btn"}${radio-btn class="Lms-playqueue-btn Lms-btn"}
|
||||
<h4><small>${nb-tracks}</small></h4>
|
||||
</div>
|
||||
${entries}
|
||||
@@ -20,8 +20,9 @@
|
||||
</div>
|
||||
</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.play-btn"><i class="fa fa-fw fa-play-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.delete-btn"><i class="fa fa-fw fa-times-circle"></i></message>
|
||||
<message id="Lms.PlayQueue.template.more-btn"><i class="fa fa-fw fa-plus-circle"></i></message>
|
||||
|
||||
<message id="Lms.PlayQueue.template.entry">
|
||||
<div class="row Lms-vertical-align Lms-explore-entry-list Lms-playqueue-entry ${is-selected}">
|
||||
@@ -39,7 +40,7 @@
|
||||
</div>
|
||||
<div class="col-xs-2 col-sm-2 col-md-2">
|
||||
<div class="Lms-playqueue-entry-controls Lms-vertical-align">
|
||||
${play-btn}${del-btn}
|
||||
${play-btn class="Lms-playqueue-btn Lms-btn"}${del-btn class="Lms-playqueue-btn Lms-btn"}<span class="hidden-xs Lms-playqueue-btn Lms-btn">${more-btn}</span>
|
||||
</div>
|
||||
<div class="Lms-playqueue-entry-duration text-muted">
|
||||
${duration}
|
||||
|
||||
+5
-5
@@ -17,7 +17,7 @@
|
||||
${<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}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
${container}
|
||||
</div>
|
||||
@@ -59,16 +59,16 @@
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry">
|
||||
<div class="row Lms-explore-release-entry Lms-entry-hoverable Lms-vertical-align ${is-playing}">
|
||||
<div class="col-xs-1 Lms-explore-release-entry-tracknumber text-muted">
|
||||
<div class="col-sm-1 col-xs-1 Lms-explore-release-entry-tracknumber text-muted">
|
||||
${<if-has-track-number>}${track-number}${</if-has-track-number>}
|
||||
</div>
|
||||
<div class="col-xs-8 Lms-explore-release-entry-name">
|
||||
<div class="col-sm-9 col-xs-7 Lms-explore-release-entry-name">
|
||||
<div class="Lms-trackname text-primary">${name}</div>
|
||||
${<if-has-artists>}${artists}${</if-has-artists>}
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<div class="col-sm-2 col-xs-4">
|
||||
<div class="Lms-explore-release-entry-controls">
|
||||
${play-btn}${more-btn}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="Lms-explore-release-entry-track-duration text-muted">
|
||||
${duration}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<message id="Lms.Explore.Releases.template">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${more-btn}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="row">
|
||||
${releases}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="Lms-header">
|
||||
${mode class="nav nav-pills Lms-explore-mode-container"}
|
||||
${play-btn}${more-btn}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
${tracks}
|
||||
${loading-indicator class="Lms-horizontal-center Lms-loading-indicator"}
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="col-xs-2 col-sm-2 col-md-2">
|
||||
<div class="Lms-explore-tracks-entry-controls">
|
||||
${play-btn}${more-btn}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
<div class="Lms-explore-tracks-entry-duration text-muted">
|
||||
${duration}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# If already in cache, be silent
|
||||
if(PSTREAMS_INCLUDE_DIRS)
|
||||
set (PSTREAMS_FIND_QUIETLY TRUE)
|
||||
endif()
|
||||
|
||||
FIND_PATH(PSTREAMS_INCLUDE_DIR NAMES pstream.h
|
||||
PATH_SUFFIXES pstreams
|
||||
HINTS ${PSTREAMS_ROOT}/include $ENV{PSTREAMS_ROOT})
|
||||
|
||||
set(PSTREAMS_INCLUDE_DIRS ${PSTREAMS_INCLUDE_DIR})
|
||||
|
||||
# Handle the QUIETLY and REQUIRED arguments and set PSTREAMS_FOUND to TRUE if
|
||||
# all listed variables are TRUE.
|
||||
INCLUDE(FindPackageHandleStandardArgs)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Pstreams DEFAULT_MSG PSTREAMS_INCLUDE_DIRS)
|
||||
|
||||
MARK_AS_ADVANCED(PSTREAMS_INCLUDE_DIRS)
|
||||
@@ -1,87 +0,0 @@
|
||||
# - Try to find the Taglib library
|
||||
# Once done this will define
|
||||
#
|
||||
# TAGLIB_FOUND - system has the taglib library
|
||||
# TAGLIB_CFLAGS - the taglib cflags
|
||||
# TAGLIB_LIBRARIES - The libraries needed to use taglib
|
||||
|
||||
# Copyright (c) 2006, Laurent Montel, <montel@kde.org>
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
|
||||
IF(TAGLIB_FOUND)
|
||||
MESSAGE(STATUS "Using manually specified taglib locations")
|
||||
ELSE()
|
||||
|
||||
if(NOT TAGLIB_MIN_VERSION)
|
||||
set(TAGLIB_MIN_VERSION "1.6")
|
||||
endif(NOT TAGLIB_MIN_VERSION)
|
||||
|
||||
if(NOT WIN32)
|
||||
find_program(TAGLIBCONFIG_EXECUTABLE NAMES taglib-config PATHS
|
||||
${BIN_INSTALL_DIR}
|
||||
)
|
||||
endif(NOT WIN32)
|
||||
|
||||
#reset vars
|
||||
set(TAGLIB_LIBRARIES)
|
||||
set(TAGLIB_CFLAGS)
|
||||
|
||||
# MESSAGE( STATUS "PATHS: ${PATHS}")
|
||||
# if taglib-config has been found
|
||||
if(TAGLIBCONFIG_EXECUTABLE)
|
||||
|
||||
exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_VERSION)
|
||||
|
||||
if(TAGLIB_VERSION VERSION_LESS "${TAGLIB_MIN_VERSION}")
|
||||
message(STATUS "TagLib version not found: version searched :${TAGLIB_MIN_VERSION}, found ${TAGLIB_VERSION}")
|
||||
set(TAGLIB_FOUND FALSE)
|
||||
else(TAGLIB_VERSION VERSION_LESS "${TAGLIB_MIN_VERSION}")
|
||||
|
||||
exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_LIBRARIES)
|
||||
|
||||
exec_program(${TAGLIBCONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE TAGLIB_CFLAGS)
|
||||
|
||||
if(TAGLIB_LIBRARIES AND TAGLIB_CFLAGS)
|
||||
set(TAGLIB_FOUND TRUE)
|
||||
# message(STATUS "Found taglib: ${TAGLIB_LIBRARIES}")
|
||||
endif(TAGLIB_LIBRARIES AND TAGLIB_CFLAGS)
|
||||
string(REGEX REPLACE " *-I" ";" TAGLIB_INCLUDES "${TAGLIB_CFLAGS}")
|
||||
endif(TAGLIB_VERSION VERSION_LESS "${TAGLIB_MIN_VERSION}")
|
||||
mark_as_advanced(TAGLIB_CFLAGS TAGLIB_LIBRARIES TAGLIB_INCLUDES)
|
||||
|
||||
else(TAGLIBCONFIG_EXECUTABLE)
|
||||
|
||||
include(FindLibraryWithDebug)
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_path(TAGLIB_INCLUDES
|
||||
NAMES
|
||||
tag.h
|
||||
PATH_SUFFIXES taglib
|
||||
PATHS
|
||||
${INCLUDE_INSTALL_DIR}
|
||||
)
|
||||
|
||||
find_library_with_debug(TAGLIB_LIBRARIES
|
||||
WIN32_DEBUG_POSTFIX d
|
||||
NAMES tag
|
||||
PATHS
|
||||
${LIB_INSTALL_DIR}
|
||||
)
|
||||
|
||||
find_package_handle_standard_args(Taglib DEFAULT_MSG
|
||||
TAGLIB_INCLUDES TAGLIB_LIBRARIES)
|
||||
endif(TAGLIBCONFIG_EXECUTABLE)
|
||||
ENDIF()
|
||||
|
||||
if(TAGLIB_FOUND)
|
||||
if(NOT Taglib_FIND_QUIETLY AND TAGLIBCONFIG_EXECUTABLE)
|
||||
message(STATUS "Found TagLib: ${TAGLIB_LIBRARIES}")
|
||||
endif(NOT Taglib_FIND_QUIETLY AND TAGLIBCONFIG_EXECUTABLE)
|
||||
else(TAGLIB_FOUND)
|
||||
if(Taglib_FIND_REQUIRED)
|
||||
message(FATAL_ERROR "Could not find Taglib")
|
||||
endif(Taglib_FIND_REQUIRED)
|
||||
endif(TAGLIB_FOUND)
|
||||
+1
-9
@@ -4,9 +4,8 @@ WORKDIR /tmp/workdir
|
||||
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG FFMPEG_VERSION=4.1.4
|
||||
ARG WT_VERSION=4.2.0
|
||||
ARG WT_VERSION=4.5.0
|
||||
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58
|
||||
ARG PSTREAMS_VERSION=1.0.1
|
||||
ARG LMS_VERSION=v3.6.3
|
||||
|
||||
ARG PREFIX="/tmp/install"
|
||||
@@ -102,13 +101,6 @@ RUN \
|
||||
make && \
|
||||
make install
|
||||
|
||||
# libpstreams
|
||||
RUN \
|
||||
DIR=/tmp/libpstreams && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://sourceforge.net/projects/pstreams/files/pstreams/Release%201.0/pstreams-${PSTREAMS_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f pstreams-${PSTREAMS_VERSION}.tar.gz && \
|
||||
prefix=${PREFIX} make install prefix=/ DESTDIR=${PREFIX}
|
||||
|
||||
# LMS
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
|
||||
+3
-8
@@ -112,9 +112,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-explore-btn {
|
||||
font-size: 24px !important;
|
||||
text-align: left;
|
||||
width: 28px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.Lms-explore-clusters {
|
||||
@@ -185,7 +183,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry {
|
||||
min-height: 32px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry:hover .Lms-explore-release-entry-controls {
|
||||
@@ -278,8 +276,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-playqueue-btn {
|
||||
font-size: 24px !important;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.Lms-playqueue-entry:hover .Lms-playqueue-entry-controls {
|
||||
@@ -350,8 +347,6 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
|
||||
.Lms-player-btn-controls {
|
||||
font-size: 40px !important;
|
||||
text-align: center;
|
||||
width: 1.1em;
|
||||
}
|
||||
|
||||
.Lms-player-controls {
|
||||
|
||||
@@ -82,7 +82,7 @@ checkUserPassword(Database::Session& session, const std::string& loginName, cons
|
||||
case Database::User::AuthMode::Internal:
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
|
||||
const Wt::Auth::BCryptHashFunction hashFunc {6}; // TODO parametrize this
|
||||
const Wt::Auth::BCryptHashFunction hashFunc {7}; // TODO parametrize this
|
||||
return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
add_library(lmsav SHARED
|
||||
impl/AvInfo.cpp
|
||||
impl/AvTranscoder.cpp
|
||||
impl/AvTranscodeResourceHandler.cpp
|
||||
impl/AvTypes.cpp
|
||||
impl/AudioFile.cpp
|
||||
impl/Transcoder.cpp
|
||||
impl/TranscodeResourceHandler.cpp
|
||||
impl/Types.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsav INTERFACE
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
@@ -44,20 +44,28 @@ static std::string averror_to_string(int error)
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
MediaFileException::MediaFileException(int avError)
|
||||
: AvException {"MediaFileException: " + averror_to_string(avError)}
|
||||
class AudioFileException : public Av::Exception
|
||||
{
|
||||
public:
|
||||
AudioFileException(int avError)
|
||||
: Av::Exception {"AudioFileException: " + averror_to_string(avError)}
|
||||
{}
|
||||
};
|
||||
|
||||
std::unique_ptr<IAudioFile>
|
||||
parseAudioFile(const std::filesystem::path& p)
|
||||
{
|
||||
return std::make_unique<AudioFile>(p);
|
||||
}
|
||||
|
||||
|
||||
MediaFile::MediaFile(const std::filesystem::path& p)
|
||||
AudioFile::AudioFile(const std::filesystem::path& p)
|
||||
: _p {p}
|
||||
{
|
||||
int error = avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr);
|
||||
int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)};
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
|
||||
throw MediaFileException(error);
|
||||
throw AudioFileException {error};
|
||||
}
|
||||
|
||||
error = avformat_find_stream_info(_context, nullptr);
|
||||
@@ -65,32 +73,32 @@ MediaFile::MediaFile(const std::filesystem::path& p)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
|
||||
avformat_close_input(&_context);
|
||||
throw MediaFileException(error);
|
||||
throw AudioFileException {error};
|
||||
}
|
||||
}
|
||||
|
||||
MediaFile::~MediaFile()
|
||||
AudioFile::~AudioFile()
|
||||
{
|
||||
avformat_close_input(&_context);
|
||||
}
|
||||
|
||||
std::string
|
||||
MediaFile::getFormatName() const
|
||||
const std::filesystem::path&
|
||||
AudioFile::getPath() const
|
||||
{
|
||||
return _context->iformat->name;
|
||||
return _p;
|
||||
}
|
||||
|
||||
std::chrono::milliseconds
|
||||
MediaFile::getDuration() const
|
||||
AudioFile::getDuration() const
|
||||
{
|
||||
if (_context->duration == AV_NOPTS_VALUE)
|
||||
return std::chrono::milliseconds(0); // TODO estimate
|
||||
return std::chrono::milliseconds {0}; // TODO estimate
|
||||
|
||||
return std::chrono::milliseconds(_context->duration / AV_TIME_BASE * 1000);
|
||||
return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000};
|
||||
}
|
||||
|
||||
void
|
||||
getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std::string>& res)
|
||||
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||
{
|
||||
if (!dictionnary)
|
||||
return;
|
||||
@@ -102,10 +110,10 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string>
|
||||
MediaFile::getMetaData(void)
|
||||
AudioFile::MetadataMap
|
||||
AudioFile::getMetaData() const
|
||||
{
|
||||
std::map<std::string, std::string> res;
|
||||
MetadataMap res;
|
||||
|
||||
getMetaDataFromDictionnary(_context->metadata, res);
|
||||
|
||||
@@ -113,7 +121,7 @@ MediaFile::getMetaData(void)
|
||||
// If we did not find tags, search metadata in streams
|
||||
if (res.empty())
|
||||
{
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
for (std::size_t i {}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
|
||||
|
||||
@@ -126,7 +134,7 @@ MediaFile::getMetaData(void)
|
||||
}
|
||||
|
||||
std::vector<StreamInfo>
|
||||
MediaFile::getStreamInfo() const
|
||||
AudioFile::getStreamInfo() const
|
||||
{
|
||||
std::vector<StreamInfo> res;
|
||||
|
||||
@@ -154,7 +162,7 @@ MediaFile::getStreamInfo() const
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
MediaFile::getBestStream() const
|
||||
AudioFile::getBestStream() const
|
||||
{
|
||||
int res = av_find_best_stream(_context,
|
||||
AVMEDIA_TYPE_AUDIO,
|
||||
@@ -170,7 +178,7 @@ MediaFile::getBestStream() const
|
||||
}
|
||||
|
||||
bool
|
||||
MediaFile::hasAttachedPictures(void) const
|
||||
AudioFile::hasAttachedPictures(void) const
|
||||
{
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
{
|
||||
@@ -182,9 +190,9 @@ MediaFile::hasAttachedPictures(void) const
|
||||
}
|
||||
|
||||
void
|
||||
MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||
AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||
{
|
||||
static const std::map<int, std::string> codecMimeMap =
|
||||
static const std::unordered_map<int, std::string> codecMimeMap =
|
||||
{
|
||||
{ AV_CODEC_ID_BMP, "image/x-bmp" },
|
||||
{ AV_CODEC_ID_GIF, "image/gif" },
|
||||
@@ -230,7 +238,7 @@ MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MediaFileFormat>
|
||||
std::optional<AudioFileFormat>
|
||||
guessMediaFileFormat(const std::filesystem::path& file)
|
||||
{
|
||||
AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)};
|
||||
@@ -252,7 +260,7 @@ guessMediaFileFormat(const std::filesystem::path& file)
|
||||
else if (mimeTypes.size() > 1)
|
||||
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'";
|
||||
|
||||
MediaFileFormat res;
|
||||
AudioFileFormat res;
|
||||
res.format = formats.front();
|
||||
res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
|
||||
struct AVFormatContext;
|
||||
|
||||
namespace Av
|
||||
{
|
||||
|
||||
class AudioFile final : public IAudioFile
|
||||
{
|
||||
public:
|
||||
AudioFile(const std::filesystem::path& p);
|
||||
~AudioFile();
|
||||
|
||||
AudioFile(const AudioFile&) = delete;
|
||||
AudioFile(AudioFile&&) = delete;
|
||||
AudioFile& operator=(const AudioFile&) = delete;
|
||||
AudioFile& operator=(AudioFile&&) = delete;
|
||||
|
||||
const std::filesystem::path& getPath() const override;
|
||||
std::chrono::milliseconds getDuration() const override;
|
||||
MetadataMap getMetaData() const override;
|
||||
std::vector<StreamInfo> getStreamInfo() const override;
|
||||
std::optional<std::size_t> getBestStream() const override;
|
||||
bool hasAttachedPictures() const override;
|
||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
||||
|
||||
private:
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context {};
|
||||
};
|
||||
|
||||
} // namespace Av
|
||||
|
||||
+20
-12
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvTranscodeResourceHandler.hpp"
|
||||
#include "TranscodeResourceHandler.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
@@ -36,24 +36,32 @@ namespace Av
|
||||
_transcoder.start();
|
||||
}
|
||||
|
||||
void
|
||||
Wt::Http::ResponseContinuation*
|
||||
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
{
|
||||
response.setMimeType(_transcoder.getOutputMimeType());
|
||||
|
||||
if (!_transcoder.isComplete())
|
||||
if (_nbBytesReady > 0)
|
||||
{
|
||||
std::vector<unsigned char> buffer;
|
||||
|
||||
_transcoder.process(buffer, _chunkSize);
|
||||
response.out().write(reinterpret_cast<const char *>(&buffer[0]), buffer.size());
|
||||
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _nbBytesReady);
|
||||
_nbBytesReady = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
TranscodeResourceHandler::isFinished() const
|
||||
{
|
||||
return _transcoder.isComplete();
|
||||
if (!_transcoder.finished())
|
||||
{
|
||||
Wt::Http::ResponseContinuation *continuation {response.createContinuation()};
|
||||
continuation->waitForMoreData();
|
||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||
{
|
||||
assert(_nbBytesReady == 0);
|
||||
_nbBytesReady = nbBytesRead;
|
||||
continuation->haveMoreData();
|
||||
});
|
||||
|
||||
return continuation;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -19,9 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
@@ -32,11 +35,11 @@ namespace Av
|
||||
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
|
||||
private:
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
||||
|
||||
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
||||
bool isFinished() const override;
|
||||
|
||||
static constexpr std::size_t _chunkSize {262144};
|
||||
static constexpr std::size_t _chunkSize {32768};
|
||||
std::array<std::byte, _chunkSize> _buffer;
|
||||
std::size_t _nbBytesReady {};
|
||||
const std::filesystem::path _trackPath;
|
||||
Transcoder _transcoder;
|
||||
};
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <iomanip>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "utils/IChildProcessManager.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -32,7 +32,7 @@ namespace Av {
|
||||
|
||||
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - "
|
||||
|
||||
static std::atomic<size_t> globalId {};
|
||||
static std::atomic<size_t> globalId {};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
void
|
||||
@@ -40,39 +40,40 @@ Transcoder::init()
|
||||
{
|
||||
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(ffmpegPath))
|
||||
throw LmsException {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters)
|
||||
: _filePath {filePath},
|
||||
_parameters {parameters},
|
||||
_id {globalId++}
|
||||
: _id {globalId++}
|
||||
, _filePath {filePath}
|
||||
, _parameters {parameters}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Transcoder::~Transcoder() = default;
|
||||
|
||||
bool
|
||||
Transcoder::start()
|
||||
{
|
||||
if (ffmpegPath.empty())
|
||||
init();
|
||||
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(_filePath))
|
||||
{
|
||||
LOG(ERROR) << "File '" << _filePath << "' does not exist!";
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
else if (!std::filesystem::is_regular_file( _filePath) )
|
||||
{
|
||||
LOG(ERROR) << "File '" << _filePath << "' is not regular!";
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what();
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,17 +83,21 @@ Transcoder::start()
|
||||
|
||||
args.emplace_back(ffmpegPath.string());
|
||||
|
||||
// Make sure we do not produce anything in the stderr output
|
||||
// Make sure:
|
||||
// - we do not produce anything in the stderr output
|
||||
// - we do not rely on input
|
||||
// in order not to block the whole forked process
|
||||
args.emplace_back("-loglevel");
|
||||
args.emplace_back("quiet");
|
||||
args.emplace_back("-nostdin");
|
||||
|
||||
// input Offset
|
||||
if (_parameters.offset)
|
||||
{
|
||||
args.emplace_back("-ss");
|
||||
args.emplace_back(std::to_string((*_parameters.offset).count()));
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000});
|
||||
args.emplace_back(oss.str());
|
||||
}
|
||||
|
||||
// Input file
|
||||
@@ -120,6 +125,7 @@ Transcoder::start()
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_parameters.bitrate));
|
||||
|
||||
|
||||
// Codecs and formats
|
||||
switch (_parameters.format)
|
||||
{
|
||||
@@ -157,7 +163,6 @@ Transcoder::start()
|
||||
break;
|
||||
|
||||
default:
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,84 +174,56 @@ Transcoder::start()
|
||||
for (const std::string& arg : args)
|
||||
LOG(DEBUG) << "Arg = '" << arg << "'";
|
||||
|
||||
// make sure only one thread is executing this part of code
|
||||
// Caution: stdin must have been closed before
|
||||
try
|
||||
{
|
||||
static std::mutex transcoderMutex;
|
||||
|
||||
std::lock_guard<std::mutex> lock {transcoderMutex};
|
||||
|
||||
_child = std::make_shared<redi::ipstream>();
|
||||
|
||||
// Caution: stdin must have been closed before
|
||||
_child->open(ffmpegPath.string(), args);
|
||||
if (!_child->is_open())
|
||||
{
|
||||
LOG(DEBUG) << "Exec failed!";
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_child->out().eof())
|
||||
{
|
||||
LOG(DEBUG) << "Early end of file!";
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
_childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
|
||||
}
|
||||
catch (ChildProcessException& exception)
|
||||
{
|
||||
LOG(ERROR) << "Cannot execute '" << ffmpegPath << "': " << exception.what();
|
||||
return false;
|
||||
}
|
||||
LOG(DEBUG) << "Stream opened!";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
Transcoder::process(std::vector<unsigned char>& output, std::size_t maxSize)
|
||||
Transcoder::asyncWaitForData(WaitCallback cb)
|
||||
{
|
||||
if (!_child || _isComplete)
|
||||
return;
|
||||
assert(_childProcess);
|
||||
|
||||
output.resize(maxSize);
|
||||
LOG(DEBUG) << "Want to wait for data";
|
||||
|
||||
//Read on the output stream
|
||||
_child->out().read(reinterpret_cast<char*>(&output[0]), maxSize);
|
||||
output.resize(_child->out().gcount());
|
||||
|
||||
if (_child->out().fail())
|
||||
_childProcess->asyncWaitForData([cb = std::move(cb)]
|
||||
{
|
||||
LOG(DEBUG) << "Stdout FAILED";
|
||||
_isComplete = true;
|
||||
}
|
||||
|
||||
if (_child->out().eof())
|
||||
{
|
||||
LOG(DEBUG) << "Stdout EOF!";
|
||||
_isComplete = true;
|
||||
}
|
||||
|
||||
if (_isComplete)
|
||||
{
|
||||
LOG(DEBUG) << "Transcode complete!";
|
||||
_child->clear();
|
||||
_child.reset();
|
||||
}
|
||||
|
||||
_total += output.size();
|
||||
|
||||
LOG(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
Transcoder::~Transcoder()
|
||||
void
|
||||
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
|
||||
{
|
||||
LOG(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total;
|
||||
assert(_childProcess);
|
||||
|
||||
if (_child)
|
||||
return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
|
||||
{
|
||||
LOG(DEBUG) << "Child still here!";
|
||||
_child->rdbuf()->kill(SIGKILL);
|
||||
LOG(DEBUG) << "Closing...";
|
||||
_child->rdbuf()->close();
|
||||
LOG(DEBUG) << "Closing DONE";
|
||||
}
|
||||
readCallback(nbBytesRead);
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(_childProcess);
|
||||
|
||||
return _childProcess->readSome(buffer, bufferSize);
|
||||
}
|
||||
|
||||
bool
|
||||
Transcoder::finished() const
|
||||
{
|
||||
return _childProcess->finished();
|
||||
}
|
||||
|
||||
} // namespace Transcode
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <filesystem>
|
||||
#include <functional>
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "av/Types.hpp"
|
||||
|
||||
class IChildProcess;
|
||||
|
||||
namespace Av
|
||||
{
|
||||
class Transcoder
|
||||
{
|
||||
public:
|
||||
Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters);
|
||||
~Transcoder();
|
||||
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
Transcoder& operator=(const Transcoder&) = delete;
|
||||
Transcoder(Transcoder&&) = delete;
|
||||
Transcoder& operator=(Transcoder&&) = delete;
|
||||
|
||||
bool start();
|
||||
|
||||
using WaitCallback = std::function<void()>;
|
||||
void asyncWaitForData(WaitCallback cb);
|
||||
|
||||
// non blocking calls
|
||||
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
|
||||
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
|
||||
|
||||
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
||||
const TranscodeParameters& getParameters() const { return _parameters; }
|
||||
|
||||
bool finished() const;
|
||||
|
||||
private:
|
||||
static void init();
|
||||
|
||||
const std::size_t _id {};
|
||||
const std::filesystem::path _filePath;
|
||||
const TranscodeParameters _parameters;
|
||||
|
||||
std::unique_ptr<IChildProcess> _childProcess;
|
||||
|
||||
bool _finished {};
|
||||
std::string _outputMimeType;
|
||||
};
|
||||
|
||||
} // namespace Av
|
||||
|
||||
@@ -17,23 +17,25 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "av/AvTypes.hpp"
|
||||
#include "av/Types.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
const char* formatToMimetype(Format format)
|
||||
namespace Av
|
||||
{
|
||||
switch (format)
|
||||
|
||||
std::string_view
|
||||
formatToMimetype(Format format)
|
||||
{
|
||||
case Format::MP3: return "audio/mpeg";
|
||||
case Format::OGG_OPUS: return "audio/opus";
|
||||
case Format::MATROSKA_OPUS: return "audio/x-matroska";
|
||||
case Format::OGG_VORBIS: return "audio/ogg";
|
||||
case Format::WEBM_VORBIS: return "audio/webm";
|
||||
switch (format)
|
||||
{
|
||||
case Format::MP3: return "audio/mpeg";
|
||||
case Format::OGG_OPUS: return "audio/opus";
|
||||
case Format::MATROSKA_OPUS: return "audio/x-matroska";
|
||||
case Format::OGG_VORBIS: return "audio/ogg";
|
||||
case Format::WEBM_VORBIS: return "audio/webm";
|
||||
}
|
||||
|
||||
throw Exception {"Invalid encoding"};
|
||||
}
|
||||
|
||||
throw AvException {"Invalid encoding"};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "AvTypes.hpp"
|
||||
|
||||
struct AVFormatContext;
|
||||
|
||||
namespace Av
|
||||
{
|
||||
|
||||
void AvInit();
|
||||
|
||||
struct Picture
|
||||
{
|
||||
std::string mimeType;
|
||||
const std::byte* data {};
|
||||
std::size_t dataSize;
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t id;
|
||||
std::size_t bitrate;
|
||||
};
|
||||
|
||||
class MediaFileException : public AvException
|
||||
{
|
||||
public:
|
||||
MediaFileException(int avError);
|
||||
};
|
||||
|
||||
class MediaFile
|
||||
{
|
||||
public:
|
||||
MediaFile(const std::filesystem::path& p);
|
||||
~MediaFile();
|
||||
|
||||
MediaFile(const MediaFile&) = delete;
|
||||
MediaFile& operator=(const MediaFile&) = delete;
|
||||
MediaFile(MediaFile&&) = delete;
|
||||
MediaFile& operator=(MediaFile&&) = delete;
|
||||
|
||||
std::string getFormatName() const;
|
||||
|
||||
const std::filesystem::path& getPath() const {return _p;};
|
||||
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
std::map<std::string, std::string> getMetaData(void);
|
||||
|
||||
std::vector<StreamInfo> getStreamInfo() const;
|
||||
std::optional<std::size_t> getBestStream() const; // none if failure/unknown
|
||||
bool hasAttachedPictures(void) const;
|
||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const;
|
||||
|
||||
private:
|
||||
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context {};
|
||||
};
|
||||
|
||||
|
||||
struct MediaFileFormat
|
||||
{
|
||||
std::string mimeType;
|
||||
std::string format;
|
||||
};
|
||||
|
||||
std::optional<MediaFileFormat> guessMediaFileFormat(const std::filesystem::path& file);
|
||||
|
||||
} // namespace Av
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <chrono>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include <pstreams/pstream.h>
|
||||
|
||||
#include "AvTypes.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
Format format;
|
||||
std::size_t bitrate {128000};
|
||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||
std::optional<std::chrono::seconds> offset;
|
||||
bool stripMetadata {true};
|
||||
};
|
||||
|
||||
class Transcoder
|
||||
{
|
||||
public:
|
||||
static void init();
|
||||
|
||||
Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters);
|
||||
~Transcoder();
|
||||
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
Transcoder& operator=(const Transcoder&) = delete;
|
||||
Transcoder(Transcoder&&) = delete;
|
||||
Transcoder& operator=(Transcoder&&) = delete;
|
||||
|
||||
bool start();
|
||||
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
||||
void process(std::vector<unsigned char>& output, std::size_t maxSize);
|
||||
bool isComplete(void) const { return _isComplete; }
|
||||
|
||||
const TranscodeParameters& getParameters() const { return _parameters; }
|
||||
|
||||
private:
|
||||
const std::filesystem::path _filePath;
|
||||
const TranscodeParameters _parameters;
|
||||
|
||||
std::shared_ptr<redi::ipstream> _child;
|
||||
|
||||
bool _isComplete {};
|
||||
std::size_t _total {};
|
||||
const std::size_t _id {};
|
||||
std::string _outputMimeType;
|
||||
};
|
||||
|
||||
} // namespace Av
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
struct Picture
|
||||
{
|
||||
std::string mimeType;
|
||||
const std::byte* data {};
|
||||
std::size_t dataSize;
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t id;
|
||||
std::size_t bitrate;
|
||||
};
|
||||
|
||||
class IAudioFile
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioFile() = default;
|
||||
|
||||
using MetadataMap = std::unordered_map<std::string, std::string>;
|
||||
|
||||
virtual const std::filesystem::path& getPath() const = 0;
|
||||
virtual std::chrono::milliseconds getDuration() const = 0;
|
||||
virtual MetadataMap getMetaData() const = 0;
|
||||
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
|
||||
virtual std::optional<std::size_t> getBestStream() const = 0; // none if failure/unknown
|
||||
virtual bool hasAttachedPictures() const = 0;
|
||||
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
|
||||
|
||||
struct AudioFileFormat
|
||||
{
|
||||
std::string mimeType;
|
||||
std::string format;
|
||||
};
|
||||
|
||||
std::optional<AudioFileFormat> guessAudioFileFormat(const std::filesystem::path& file);
|
||||
|
||||
} // namespace Av
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <chrono>
|
||||
#include <optional>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
Format format;
|
||||
std::size_t bitrate {128000};
|
||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||
std::chrono::milliseconds offset {0};
|
||||
bool stripMetadata {true};
|
||||
};
|
||||
|
||||
} // namespace Av
|
||||
|
||||
-1
@@ -29,6 +29,5 @@ namespace Av
|
||||
struct TranscodeParameters;
|
||||
|
||||
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
|
||||
}
|
||||
|
||||
@@ -19,29 +19,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
class AvException : public LmsException
|
||||
{
|
||||
public:
|
||||
AvException(const std::string& msg) : LmsException(msg) {}
|
||||
};
|
||||
class Exception : public LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
enum class Format
|
||||
{
|
||||
// Values are important and must not be changed
|
||||
MP3 = 0,
|
||||
OGG_OPUS = 1,
|
||||
MATROSKA_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
};
|
||||
|
||||
const char* formatToMimetype(Format encoding);
|
||||
enum class Format
|
||||
{
|
||||
// Values are important and must not be changed (stored in the UI's localstorage)
|
||||
MP3 = 0,
|
||||
OGG_OPUS = 1,
|
||||
MATROSKA_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
};
|
||||
|
||||
std::string_view formatToMimetype(Format format);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,7 @@ elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++)
|
||||
impl/graphicsmagick/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_GM")
|
||||
target_include_directories(lmscover PRIVATE ${GRAPHICSMAGICKXX_INCLUDE_DIRS})
|
||||
target_compile_options(lmscover PRIVATE ${GRAPHICSMAGICKXX_CFLAGS_OTHER})
|
||||
target_link_libraries(lmscover PRIVATE ${GRAPHICSMAGICKXX_LIBRARIES})
|
||||
target_link_libraries(lmscover PRIVATE PkgConfig::GraphicsMagick++)
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
|
||||
endif()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "CoverArtGrabber.hpp"
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
@@ -125,7 +125,7 @@ Grabber::Grabber(const std::filesystem::path& execPath,
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const
|
||||
Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
@@ -303,10 +303,9 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
|
||||
try
|
||||
{
|
||||
const Av::MediaFile input {p};
|
||||
image = getFromAvMediaFile(input, width);
|
||||
image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
|
||||
}
|
||||
catch (Av::AvException& e)
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Database
|
||||
|
||||
namespace Av
|
||||
{
|
||||
class MediaFile;
|
||||
class IAudioFile;
|
||||
}
|
||||
|
||||
namespace CoverArt
|
||||
@@ -106,7 +106,7 @@ namespace CoverArt
|
||||
void flushCache() override;
|
||||
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback);
|
||||
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const;
|
||||
|
||||
std::unique_ptr<IEncodedImage> getFromTrack(const std::filesystem::path& path, ImageSize width) const;
|
||||
|
||||
@@ -45,7 +45,10 @@ Artist::getByName(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().find<Artist>().where("name = ?").bind( std::string{name, 0, _maxNameLength} );
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string {name, 0, _maxNameLength})
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 27
|
||||
#define LMS_DATABASE_VERSION 28
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -284,6 +284,12 @@ CREATE TABLE "user_backup" (
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 27)
|
||||
{
|
||||
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
|
||||
// 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";
|
||||
|
||||
@@ -41,7 +41,8 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
Never = 0,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly
|
||||
Monthly,
|
||||
Hourly,
|
||||
};
|
||||
|
||||
// Do not modify values (just add)
|
||||
@@ -56,13 +57,13 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
static pointer get(Session& session);
|
||||
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<Wt::Dbo::ptr<ClusterType>> getClusterTypes() const;
|
||||
std::unordered_set<std::filesystem::path> getAudioFileExtensions() const;
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
|
||||
// Setters
|
||||
void addAudioFileExtension(const std::filesystem::path& ext);
|
||||
|
||||
@@ -14,7 +14,7 @@ target_include_directories(lmsmetadata PRIVATE
|
||||
|
||||
target_link_libraries(lmsmetadata PRIVATE
|
||||
lmsav
|
||||
tag
|
||||
PkgConfig::Taglib
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PUBLIC
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
@@ -30,11 +30,9 @@
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
using MetadataMap = std::map<std::string, std::string>;
|
||||
|
||||
template <typename T>
|
||||
std::optional<T>
|
||||
findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
|
||||
if (it == std::cend(metadataMap))
|
||||
@@ -45,7 +43,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
|
||||
|
||||
template <>
|
||||
std::optional<std::vector<UUID>>
|
||||
findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)};
|
||||
if (!str)
|
||||
@@ -69,7 +67,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
|
||||
|
||||
static
|
||||
std::optional<Album>
|
||||
getAlbum(const MetadataMap& metadataMap)
|
||||
getAlbum(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Album> res;
|
||||
|
||||
@@ -84,7 +82,7 @@ getAlbum(const MetadataMap& metadataMap)
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getAlbumArtists(const MetadataMap& metadataMap)
|
||||
getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
@@ -99,7 +97,7 @@ getAlbumArtists(const MetadataMap& metadataMap)
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getArtists(const MetadataMap& metadataMap)
|
||||
getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
|
||||
@@ -133,31 +131,28 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
try
|
||||
{
|
||||
Av::MediaFile mediaFile {p};
|
||||
const auto mediaFile {Av::parseAudioFile(p)};
|
||||
|
||||
// Stream info
|
||||
{
|
||||
std::vector<AudioStream> audioStreams;
|
||||
|
||||
for (auto stream : mediaFile.getStreamInfo())
|
||||
for (auto stream : mediaFile->getStreamInfo())
|
||||
{
|
||||
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)};
|
||||
track.audioStreams.emplace_back(audioStream);
|
||||
}
|
||||
}
|
||||
|
||||
track.duration = mediaFile.getDuration();
|
||||
track.hasCover = mediaFile.hasAttachedPictures();
|
||||
track.duration = mediaFile->getDuration();
|
||||
track.hasCover = mediaFile->hasAttachedPictures();
|
||||
|
||||
MetaData::Clusters clusters;
|
||||
|
||||
const std::map<std::string, std::string> metadataMap {mediaFile.getMetaData()};
|
||||
const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
|
||||
|
||||
for (const auto& metadata : metadataMap)
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
const std::string& tag {metadata.first};
|
||||
const std::string& value {metadata.second};
|
||||
|
||||
if (debug)
|
||||
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
|
||||
|
||||
@@ -231,7 +226,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.album = getAlbum(metadataMap);
|
||||
track.albumArtists = getAlbumArtists(metadataMap);
|
||||
}
|
||||
catch(Av::MediaFileException& e)
|
||||
catch(Av::Exception& e)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
add_library(lmsscanner SHARED
|
||||
impl/AcousticBrainzUtils.cpp
|
||||
impl/MediaScanner.cpp
|
||||
impl/MediaScannerStats.cpp
|
||||
impl/Scanner.cpp
|
||||
impl/ScannerStats.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsscanner INTERFACE
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MediaScanner.hpp"
|
||||
#include "Scanner.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <boost/asio/placeholders.hpp>
|
||||
@@ -121,7 +121,7 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo)
|
||||
getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
@@ -148,11 +148,11 @@ getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artist
|
||||
for (const Artist::pointer& sameNamedArtist : Artist::getByName(session, artistInfo.name))
|
||||
{
|
||||
// Do not fallback on artist that is correctly tagged
|
||||
if (!sameNamedArtist->getMBID())
|
||||
{
|
||||
artist = sameNamedArtist;
|
||||
break;
|
||||
}
|
||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||
continue;
|
||||
|
||||
artist = sameNamedArtist;
|
||||
break;
|
||||
}
|
||||
|
||||
// No Artist found with the same name and without MBID -> creating
|
||||
@@ -242,13 +242,13 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
std::unique_ptr<IMediaScanner>
|
||||
createMediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine)
|
||||
std::unique_ptr<IScanner>
|
||||
createScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine)
|
||||
{
|
||||
return std::make_unique<MediaScanner>(db, recommendationEngine);
|
||||
return std::make_unique<Scanner>(db, recommendationEngine);
|
||||
}
|
||||
|
||||
MediaScanner::MediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine)
|
||||
Scanner::Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine)
|
||||
: _recommendationEngine {recommendationEngine}
|
||||
, _dbSession {db}
|
||||
{
|
||||
@@ -262,14 +262,14 @@ MediaScanner::MediaScanner(Database::Db& db, Recommendation::IEngine& recommenda
|
||||
start();
|
||||
}
|
||||
|
||||
MediaScanner::~MediaScanner()
|
||||
Scanner::~Scanner()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Shutting down MediaScanner...";
|
||||
LMS_LOG(DBUPDATER, INFO) << "Shutting down Scanner...";
|
||||
stop();
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::start()
|
||||
Scanner::start()
|
||||
{
|
||||
std::scoped_lock lock {_controlMutex};
|
||||
|
||||
@@ -290,7 +290,7 @@ MediaScanner::start()
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::stop()
|
||||
Scanner::stop()
|
||||
{
|
||||
std::scoped_lock lock {_controlMutex};
|
||||
|
||||
@@ -301,7 +301,7 @@ MediaScanner::stop()
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::abortScan()
|
||||
Scanner::abortScan()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
|
||||
std::scoped_lock lock {_controlMutex};
|
||||
@@ -319,7 +319,7 @@ MediaScanner::abortScan()
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::requestImmediateScan(bool force)
|
||||
Scanner::requestImmediateScan(bool force)
|
||||
{
|
||||
abortScan();
|
||||
_ioService.post([=]()
|
||||
@@ -332,7 +332,7 @@ MediaScanner::requestImmediateScan(bool force)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::requestReload()
|
||||
Scanner::requestReload()
|
||||
{
|
||||
abortScan();
|
||||
_ioService.post([=]()
|
||||
@@ -344,8 +344,8 @@ MediaScanner::requestReload()
|
||||
});
|
||||
}
|
||||
|
||||
MediaScanner::Status
|
||||
MediaScanner::getStatus() const
|
||||
Scanner::Status
|
||||
Scanner::getStatus() const
|
||||
{
|
||||
Status res;
|
||||
|
||||
@@ -360,7 +360,7 @@ MediaScanner::getStatus() const
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::scheduleNextScan()
|
||||
Scanner::scheduleNextScan()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan";
|
||||
|
||||
@@ -368,28 +368,32 @@ MediaScanner::scheduleNextScan()
|
||||
|
||||
const Wt::WDateTime now {Wt::WLocalDateTime::currentServerDateTime().toUTC()};
|
||||
|
||||
Wt::WDate nextScanDate;
|
||||
Wt::WDateTime nextScanDateTime;
|
||||
switch (_updatePeriod)
|
||||
{
|
||||
case ScanSettings::UpdatePeriod::Daily:
|
||||
if (now.time() < _startTime)
|
||||
nextScanDate = now.date();
|
||||
nextScanDateTime = {now.date(), _startTime};
|
||||
else
|
||||
nextScanDate = now.date().addDays(1);
|
||||
nextScanDateTime = {now.date().addDays(1), _startTime};
|
||||
break;
|
||||
|
||||
case ScanSettings::UpdatePeriod::Weekly:
|
||||
if (now.time() < _startTime && now.date().dayOfWeek() == 1)
|
||||
nextScanDate = now.date();
|
||||
nextScanDateTime = {now.date(), _startTime};
|
||||
else
|
||||
nextScanDate = getNextMonday(now.date());
|
||||
nextScanDateTime = {getNextMonday(now.date()), _startTime};
|
||||
break;
|
||||
|
||||
case ScanSettings::UpdatePeriod::Monthly:
|
||||
if (now.time() < _startTime && now.date().day() == 1)
|
||||
nextScanDate = now.date();
|
||||
nextScanDateTime = {now.date(), _startTime};
|
||||
else
|
||||
nextScanDate = getNextFirstOfMonth(now.date());
|
||||
nextScanDateTime = {getNextFirstOfMonth(now.date()), _startTime};
|
||||
break;
|
||||
|
||||
case ScanSettings::UpdatePeriod::Hourly:
|
||||
nextScanDateTime = {now.date(), now.time().addSecs(3600)};
|
||||
break;
|
||||
|
||||
case ScanSettings::UpdatePeriod::Never:
|
||||
@@ -397,13 +401,8 @@ MediaScanner::scheduleNextScan()
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::WDateTime nextScanDateTime;
|
||||
|
||||
if (nextScanDate.isValid())
|
||||
{
|
||||
nextScanDateTime = Wt::WDateTime {nextScanDate, _startTime};
|
||||
if (nextScanDateTime.isValid())
|
||||
scheduleScan(false, nextScanDateTime);
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock {_statusMutex};
|
||||
@@ -411,11 +410,11 @@ MediaScanner::scheduleNextScan()
|
||||
_nextScheduledScan = nextScanDateTime;
|
||||
}
|
||||
|
||||
_sigScheduled.emit(_nextScheduledScan);
|
||||
_events.scanScheduled.emit(_nextScheduledScan);
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::countAllFiles(ScanStats& stats)
|
||||
Scanner::countAllFiles(ScanStats& stats)
|
||||
{
|
||||
ScanStepStats stepStats{stats.startTime, ScanProgressStep::DiscoveringFiles};
|
||||
|
||||
@@ -440,7 +439,7 @@ MediaScanner::countAllFiles(ScanStats& stats)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
||||
Scanner::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
auto cb {[=](boost::system::error_code ec)
|
||||
{
|
||||
@@ -469,9 +468,9 @@ MediaScanner::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::scan(bool forceScan)
|
||||
Scanner::scan(bool forceScan)
|
||||
{
|
||||
scanStarted().emit();
|
||||
_events.scanStarted.emit();
|
||||
|
||||
{
|
||||
std::unique_lock lock {_statusMutex};
|
||||
@@ -517,14 +516,14 @@ MediaScanner::scan(bool forceScan)
|
||||
{
|
||||
std::unique_lock lock {_statusMutex};
|
||||
|
||||
_lastCompleteScanStats = std::move(stats);
|
||||
_lastCompleteScanStats = stats;
|
||||
_currentScanStepStats.reset();
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
|
||||
scheduleNextScan();
|
||||
|
||||
scanComplete().emit();
|
||||
_events.scanComplete.emit(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -538,7 +537,7 @@ MediaScanner::scan(bool forceScan)
|
||||
}
|
||||
|
||||
bool
|
||||
MediaScanner::fetchTrackFeatures(Database::IdType trackId, const UUID& MBID)
|
||||
Scanner::fetchTrackFeatures(Database::IdType trackId, const UUID& MBID)
|
||||
{
|
||||
std::map<std::string, double> features;
|
||||
|
||||
@@ -564,7 +563,7 @@ MediaScanner::fetchTrackFeatures(Database::IdType trackId, const UUID& MBID)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::fetchTrackFeatures(ScanStats& stats)
|
||||
Scanner::fetchTrackFeatures(ScanStats& stats)
|
||||
{
|
||||
if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features)
|
||||
return;
|
||||
@@ -614,7 +613,7 @@ MediaScanner::fetchTrackFeatures(ScanStats& stats)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::refreshScanSettings()
|
||||
Scanner::refreshScanSettings()
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -646,7 +645,7 @@ MediaScanner::refreshScanSettings()
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::notifyInProgress(const ScanStepStats& stepStats)
|
||||
Scanner::notifyInProgress(const ScanStepStats& stepStats)
|
||||
{
|
||||
{
|
||||
std::unique_lock lock {_statusMutex};
|
||||
@@ -654,12 +653,12 @@ MediaScanner::notifyInProgress(const ScanStepStats& stepStats)
|
||||
}
|
||||
|
||||
const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
|
||||
_sigScanInProgress(stepStats);
|
||||
_events.scanInProgress(stepStats);
|
||||
_lastScanInProgressEmit = now;
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
|
||||
Scanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
|
||||
{
|
||||
std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
|
||||
|
||||
@@ -668,7 +667,7 @@ MediaScanner::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats)
|
||||
Scanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats)
|
||||
{
|
||||
Wt::WDateTime lastWriteTime;
|
||||
try
|
||||
@@ -771,28 +770,31 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
assert(track);
|
||||
|
||||
track.modify()->clearArtistLinks();
|
||||
for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists))
|
||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||
for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists, false))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist));
|
||||
|
||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists))
|
||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists, false))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist));
|
||||
|
||||
for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists))
|
||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||
for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor));
|
||||
|
||||
for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists))
|
||||
for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer));
|
||||
|
||||
for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists))
|
||||
for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist));
|
||||
|
||||
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists))
|
||||
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer));
|
||||
|
||||
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists))
|
||||
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer));
|
||||
|
||||
for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists))
|
||||
for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists, true))
|
||||
track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer));
|
||||
|
||||
track.modify()->setScanVersion(_scanVersion);
|
||||
@@ -828,7 +830,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats)
|
||||
Scanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats)
|
||||
{
|
||||
ScanStepStats stepStats{stats.startTime, ScanProgressStep::ScanningFiles};
|
||||
stepStats.totalElems = stats.filesScanned;
|
||||
@@ -886,7 +888,6 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
@@ -896,7 +897,7 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::removeMissingTracks(ScanStats& stats)
|
||||
Scanner::removeMissingTracks(ScanStats& stats)
|
||||
{
|
||||
static constexpr std::size_t batchSize {50};
|
||||
|
||||
@@ -963,7 +964,7 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::removeOrphanEntries()
|
||||
Scanner::removeOrphanEntries()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
|
||||
{
|
||||
@@ -1006,7 +1007,7 @@ MediaScanner::removeOrphanEntries()
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats)
|
||||
Scanner::checkDuplicatedAudioFiles(ScanStats& stats)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files";
|
||||
|
||||
@@ -1026,7 +1027,7 @@ MediaScanner::checkDuplicatedAudioFiles(ScanStats& stats)
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::reloadSimilarityEngine(ScanStats& stats)
|
||||
Scanner::reloadSimilarityEngine(ScanStats& stats)
|
||||
{
|
||||
ScanStepStats stepStats {stats.startTime, ScanProgressStep::ReloadingSimilarityEngine};
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "scanner/IMediaScanner.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
|
||||
class UUID;
|
||||
|
||||
@@ -44,29 +44,24 @@ namespace Recommendation
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
class MediaScanner : public IMediaScanner
|
||||
class Scanner : public IScanner
|
||||
{
|
||||
public:
|
||||
MediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine);
|
||||
~MediaScanner();
|
||||
Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine);
|
||||
~Scanner();
|
||||
|
||||
MediaScanner(const MediaScanner&) = delete;
|
||||
MediaScanner(MediaScanner&&) = delete;
|
||||
MediaScanner& operator=(const MediaScanner&) = delete;
|
||||
MediaScanner& operator=(MediaScanner&&) = delete;
|
||||
Scanner(const Scanner&) = delete;
|
||||
Scanner(Scanner&&) = delete;
|
||||
Scanner& operator=(const Scanner&) = delete;
|
||||
Scanner& operator=(Scanner&&) = delete;
|
||||
|
||||
void requestReload() override;
|
||||
void requestImmediateScan(bool force) override;
|
||||
|
||||
Status getStatus() const override;
|
||||
|
||||
Wt::Signal<>& scanStarted() override { return _sigScanStarted; }
|
||||
Wt::Signal<>& scanComplete() override { return _sigScanComplete; }
|
||||
Wt::Signal<ScanStepStats>& scanInProgress() override { return _sigScanInProgress; }
|
||||
Wt::Signal<Wt::WDateTime>& scheduled() override { return _sigScheduled; }
|
||||
Status getStatus() const override;
|
||||
Events& getEvents() override { return _events; }
|
||||
|
||||
private:
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
@@ -102,11 +97,8 @@ class MediaScanner : public IMediaScanner
|
||||
std::atomic<bool> _abortScan {};
|
||||
Wt::WIOService _ioService;
|
||||
boost::asio::system_timer _scheduleTimer {_ioService};
|
||||
Wt::Signal<> _sigScanStarted;
|
||||
Wt::Signal<> _sigScanComplete;
|
||||
Wt::Signal<ScanStepStats> _sigScanInProgress;
|
||||
Events _events;
|
||||
std::chrono::system_clock::time_point _lastScanInProgressEmit {};
|
||||
Wt::Signal<Wt::WDateTime> _sigScheduled;
|
||||
Database::Session _dbSession;
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
|
||||
@@ -123,8 +115,7 @@ class MediaScanner : public IMediaScanner
|
||||
std::unordered_set<std::filesystem::path> _fileExtensions;
|
||||
std::filesystem::path _mediaDirectory;
|
||||
Database::ScanSettings::RecommendationEngineType _recommendationEngineType;
|
||||
|
||||
}; // class MediaScanner
|
||||
};
|
||||
|
||||
} // Scanner
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "scanner/MediaScannerStats.hpp"
|
||||
#include "scanner/ScannerStats.hpp"
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <optional>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "MediaScannerStats.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IEngine;
|
||||
}
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
class IMediaScanner
|
||||
{
|
||||
public:
|
||||
virtual ~IMediaScanner() = default;
|
||||
|
||||
// Async requests
|
||||
virtual void requestReload() = 0;
|
||||
virtual void requestImmediateScan(bool force) = 0;
|
||||
|
||||
enum class State
|
||||
{
|
||||
NotScheduled,
|
||||
Scheduled,
|
||||
InProgress,
|
||||
};
|
||||
|
||||
struct Status
|
||||
{
|
||||
State currentState {State::NotScheduled};
|
||||
Wt::WDateTime nextScheduledScan;
|
||||
std::optional<ScanStats> lastCompleteScanStats;
|
||||
std::optional<ScanStepStats> currentScanStepStats;
|
||||
};
|
||||
|
||||
virtual Status getStatus() const = 0;
|
||||
|
||||
// Called just after scan start
|
||||
virtual Wt::Signal<>& scanStarted() = 0;
|
||||
|
||||
// Called just after scan complete
|
||||
virtual Wt::Signal<>& scanComplete() = 0;
|
||||
|
||||
// Called during scan in progress
|
||||
virtual Wt::Signal<ScanStepStats>& scanInProgress() = 0;
|
||||
|
||||
// Called after a schedule
|
||||
virtual Wt::Signal<Wt::WDateTime>& scheduled() = 0;
|
||||
|
||||
};
|
||||
|
||||
std::unique_ptr<IMediaScanner> createMediaScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine);
|
||||
|
||||
} // Scanner
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <optional>
|
||||
|
||||
#include "ScannerEvents.hpp"
|
||||
#include "ScannerStats.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IEngine;
|
||||
}
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
|
||||
class IScanner
|
||||
{
|
||||
public:
|
||||
virtual ~IScanner() = default;
|
||||
|
||||
// Async requests
|
||||
virtual void requestReload() = 0;
|
||||
virtual void requestImmediateScan(bool force) = 0;
|
||||
|
||||
enum class State
|
||||
{
|
||||
NotScheduled,
|
||||
Scheduled,
|
||||
InProgress,
|
||||
};
|
||||
|
||||
struct Status
|
||||
{
|
||||
State currentState {State::NotScheduled};
|
||||
Wt::WDateTime nextScheduledScan;
|
||||
std::optional<ScanStats> lastCompleteScanStats;
|
||||
std::optional<ScanStepStats> currentScanStepStats;
|
||||
};
|
||||
|
||||
virtual Status getStatus() const = 0;
|
||||
|
||||
virtual Events& getEvents() = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScanner> createScanner(Database::Db& db, Recommendation::IEngine& recommendationEngine);
|
||||
|
||||
} // Scanner
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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/WDateTime.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "ScannerStats.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
|
||||
struct Events
|
||||
{
|
||||
// Called just after scan start
|
||||
Wt::Signal<> scanStarted;
|
||||
|
||||
// Called just after scan complete (true if changes have been made)
|
||||
Wt::Signal<ScanStats> scanComplete;
|
||||
|
||||
// Called during scan in progress
|
||||
Wt::Signal<ScanStepStats> scanInProgress;
|
||||
|
||||
// Called after a schedule
|
||||
Wt::Signal<Wt::WDateTime> scanScheduled;
|
||||
};
|
||||
|
||||
} // ns Scanner
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "Scan.hpp"
|
||||
|
||||
#include "scanner/IMediaScanner.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace API::Subsonic::Scan
|
||||
@@ -32,10 +32,10 @@ namespace API::Subsonic::Scan
|
||||
{
|
||||
Response::Node statusResponse;
|
||||
|
||||
const IMediaScanner::Status scanStatus {Service<IMediaScanner>::get()->getStatus()};
|
||||
const IScanner::Status scanStatus {Service<IScanner>::get()->getStatus()};
|
||||
|
||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress);
|
||||
if (scanStatus.currentState == IMediaScanner::State::InProgress)
|
||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IScanner::State::InProgress);
|
||||
if (scanStatus.currentState == IScanner::State::InProgress)
|
||||
{
|
||||
std::size_t count{};
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace API::Subsonic::Scan
|
||||
Response
|
||||
handleStartScan(RequestContext& context)
|
||||
{
|
||||
Service<IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<IScanner>::get()->requestImmediateScan(false);
|
||||
|
||||
Response response {Response::createOkResponse(context)};
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
#include "Stream.hpp"
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "av/AvTranscodeResourceHandlerCreator.hpp"
|
||||
#include "av/AvTypes.hpp"
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||
#include "av/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
@@ -113,7 +113,7 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
Wt::Http::ResponseContinuation* continuation {request.continuation()};
|
||||
if (!continuation)
|
||||
{
|
||||
// Mandatory params
|
||||
@@ -137,12 +137,9 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
resourceHandler->processRequest(request, response);
|
||||
if (!resourceHandler->isFinished())
|
||||
{
|
||||
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -150,7 +147,7 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
||||
if (!continuation)
|
||||
{
|
||||
StreamParameters streamParameters {getStreamParameters(context)};
|
||||
@@ -164,12 +161,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
resourceHandler->processRequest(request, response);
|
||||
if (!resourceHandler->isFinished())
|
||||
{
|
||||
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ using namespace Database;
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
||||
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
@@ -377,7 +378,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const
|
||||
{
|
||||
albumNode.setAttribute("name", release->getName());
|
||||
albumNode.setAttribute("songCount", release->getTracksCount());
|
||||
albumNode.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(release->getDuration()).count()));
|
||||
albumNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(release->getDuration()).count());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -936,7 +937,10 @@ Response
|
||||
handleGetArtistsRequest(RequestContext& context)
|
||||
{
|
||||
Response response {Response::createOkResponse(context)};
|
||||
|
||||
Response::Node& artistsNode {response.createNode("artists")};
|
||||
artistsNode.setAttribute("ignoredArticles", "");
|
||||
artistsNode.setAttribute("lastModified", reportedDummyDateULong);
|
||||
|
||||
Response::Node& indexNode {artistsNode.createArrayChild("index")};
|
||||
indexNode.setAttribute("name", "?");
|
||||
@@ -1083,7 +1087,10 @@ Response
|
||||
handleGetIndexesRequest(RequestContext& context)
|
||||
{
|
||||
Response response {Response::createOkResponse(context)};
|
||||
|
||||
Response::Node& artistsNode {response.createNode("indexes")};
|
||||
artistsNode.setAttribute("ignoredArticles", "");
|
||||
artistsNode.setAttribute("lastModified", reportedDummyDateULong);
|
||||
|
||||
Response::Node& indexNode {artistsNode.createArrayChild("index")};
|
||||
indexNode.setAttribute("name", "?");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
add_library(lmsutils SHARED
|
||||
impl/ChildProcess.cpp
|
||||
impl/ChildProcessManager.cpp
|
||||
impl/Config.cpp
|
||||
impl/FileResourceHandler.cpp
|
||||
impl/Logger.cpp
|
||||
@@ -22,7 +24,7 @@ target_include_directories(lmsutils PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmsutils PRIVATE
|
||||
config++
|
||||
PkgConfig::Config++
|
||||
)
|
||||
|
||||
target_link_libraries(lmsutils PUBLIC
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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 "ChildProcess.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <stdexcept>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
#include <boost/asio/read.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
class SystemException : public ChildProcessException
|
||||
{
|
||||
public:
|
||||
SystemException(int err, const std::string& errMsg)
|
||||
: ChildProcessException {errMsg + ": " + strerror(err)}
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args)
|
||||
: _ioContext {ioContext}
|
||||
, _childStdout {_ioContext}
|
||||
|
||||
{
|
||||
// make sure only one thread is executing this part of code
|
||||
static std::mutex mutex;
|
||||
std::unique_lock<std::mutex> lock {mutex};
|
||||
|
||||
int pipe[2];
|
||||
|
||||
int res {pipe2(pipe, O_NONBLOCK | O_CLOEXEC)};
|
||||
if (res < 0)
|
||||
throw SystemException {errno, "pipe2 failed!"};
|
||||
|
||||
{
|
||||
const std::size_t pipeSize {65536*8};
|
||||
|
||||
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
|
||||
throw SystemException {errno, "fcntl failed!"};
|
||||
|
||||
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
|
||||
throw SystemException {errno, "fcntl failed!"};
|
||||
}
|
||||
|
||||
res = fork();
|
||||
if (res == -1)
|
||||
throw SystemException {errno, "fork failed!"};
|
||||
|
||||
if (res == 0) // CHILD
|
||||
{
|
||||
close(pipe[0]);
|
||||
close(STDIN_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
|
||||
// Replace stdout with pipe write
|
||||
if (dup2(pipe[1], STDOUT_FILENO) == -1)
|
||||
exit(-1);
|
||||
|
||||
std::vector<const char*> execArgs;
|
||||
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
|
||||
execArgs.push_back(nullptr);
|
||||
|
||||
res = execv(path.string().c_str(), (char *const*)&execArgs[0]);
|
||||
if (res == -1)
|
||||
exit(-1);
|
||||
}
|
||||
else // PARENT
|
||||
{
|
||||
close(pipe[1]);
|
||||
{
|
||||
boost::system::error_code assignError;
|
||||
_childStdout.assign(pipe[0], assignError);
|
||||
if (assignError)
|
||||
LMS_LOG(CHILDPROCESS, ERROR) << "Assign failed: " << assignError.message();
|
||||
}
|
||||
_childPID = res;
|
||||
}
|
||||
}
|
||||
|
||||
ChildProcess::~ChildProcess()
|
||||
{
|
||||
if (!_waited)
|
||||
{
|
||||
{
|
||||
boost::system::error_code closeError;
|
||||
_childStdout.close(closeError);
|
||||
if (closeError)
|
||||
LMS_LOG(CHILDPROCESS, ERROR) << "Closed failed: " << closeError.message();
|
||||
}
|
||||
kill();
|
||||
wait(true);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ChildProcess::drain()
|
||||
{
|
||||
char buf[128];
|
||||
|
||||
while (boost::asio::read(_childStdout, boost::asio::buffer(buf)) > 0)
|
||||
LMS_LOG(CHILDPROCESS, DEBUG) << "drained some bytes" << std::endl;
|
||||
}
|
||||
|
||||
void
|
||||
ChildProcess::kill()
|
||||
{
|
||||
::kill(_childPID, SIGKILL);
|
||||
}
|
||||
|
||||
bool
|
||||
ChildProcess::wait(bool block)
|
||||
{
|
||||
int wstatus {};
|
||||
|
||||
pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)};
|
||||
|
||||
if (pid == -1)
|
||||
throw SystemException {errno, "waitpid failed!"};
|
||||
else if (pid == 0)
|
||||
return false;
|
||||
|
||||
if (WIFEXITED(wstatus))
|
||||
_exitCode = WEXITSTATUS(wstatus);
|
||||
|
||||
_waited = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
||||
{
|
||||
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
|
||||
[this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred)
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Async read cb - ec = '" << error.message() << "', bytesTransferred = " << bytesTransferred;
|
||||
|
||||
if (error)
|
||||
{
|
||||
if (error == boost::asio::error::operation_aborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
boost::system::error_code closeError;
|
||||
_childStdout.close(closeError);
|
||||
}
|
||||
|
||||
if (error == boost::asio::error::eof)
|
||||
{
|
||||
callback(ReadResult::EndOfFile, bytesTransferred);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(ReadResult::Error, bytesTransferred);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback(ReadResult::Success, bytesTransferred);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
ChildProcess::asyncWaitForData(WaitCallback cb)
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Async wait requested";
|
||||
|
||||
_childStdout.async_wait(boost::asio::posix::stream_descriptor::wait_read,
|
||||
[cb {std::move(cb)}](const boost::system::error_code& ec)
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, DEBUG) << "Wait CB, error = " << ec.message();
|
||||
if (!ec)
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
const std::size_t res {_childStdout.read_some(boost::asio::buffer(data, bufferSize), ec)};
|
||||
LMS_LOG(CHILDPROCESS, DEBUG) << "read some " << res << " bytes, ec = " << ec.message();
|
||||
if (ec)
|
||||
_childStdout.close(ec);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool
|
||||
ChildProcess::finished()
|
||||
{
|
||||
return !_childStdout.is_open();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 <cstddef>
|
||||
#include <functional>
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||
|
||||
#include "utils/IChildProcess.hpp"
|
||||
|
||||
|
||||
class ChildProcess : public IChildProcess
|
||||
{
|
||||
public:
|
||||
~ChildProcess();
|
||||
ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args);
|
||||
|
||||
private:
|
||||
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override;
|
||||
void asyncWaitForData(WaitCallback cb) override;
|
||||
std::size_t readSome(std::byte* data, std::size_t bufferSize) override;
|
||||
bool finished() override;
|
||||
|
||||
void kill();
|
||||
void drain();
|
||||
bool wait(bool block); // return true if waited
|
||||
|
||||
using FileDescriptor = boost::asio::posix::stream_descriptor;
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
FileDescriptor _childStdout;
|
||||
::pid_t _childPID {};
|
||||
bool _waited {};
|
||||
std::optional<int> _exitCode;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 "ChildProcessManager.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "ChildProcess.hpp"
|
||||
|
||||
|
||||
std::unique_ptr<IChildProcessManager>
|
||||
createChildProcessManager()
|
||||
{
|
||||
return std::make_unique<ChildProcessManager>();
|
||||
}
|
||||
|
||||
ChildProcessManager::ChildProcessManager()
|
||||
: _work {boost::asio::make_work_guard(_ioContext)}
|
||||
{
|
||||
start();
|
||||
}
|
||||
|
||||
ChildProcessManager::~ChildProcessManager()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void
|
||||
ChildProcessManager::start()
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, INFO) << "Starting child process manager...";
|
||||
|
||||
_thread = std::make_unique<std::thread>([&]()
|
||||
{
|
||||
_ioContext.run();
|
||||
});
|
||||
|
||||
LMS_LOG(CHILDPROCESS, INFO) << "Child process manager started!";
|
||||
}
|
||||
|
||||
void
|
||||
ChildProcessManager::stop()
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, INFO) << "Stopping child process manager";
|
||||
_work.reset();
|
||||
_thread->join();
|
||||
LMS_LOG(CHILDPROCESS, INFO) << "Stopped child process manager";
|
||||
}
|
||||
|
||||
std::unique_ptr<IChildProcess>
|
||||
ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
|
||||
{
|
||||
return std::make_unique<ChildProcess>(_ioContext, path, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 <thread>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
|
||||
#include "utils/IChildProcessManager.hpp"
|
||||
|
||||
class ChildProcessManager : public IChildProcessManager
|
||||
{
|
||||
public:
|
||||
ChildProcessManager();
|
||||
~ChildProcessManager();
|
||||
|
||||
ChildProcessManager(const ChildProcessManager&) = delete;
|
||||
ChildProcessManager(ChildProcessManager&&) = delete;
|
||||
ChildProcessManager& operator=(const ChildProcessManager&) = delete;
|
||||
ChildProcessManager& operator=(ChildProcessManager&&) = delete;
|
||||
|
||||
private:
|
||||
std::unique_ptr<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) override;
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
boost::asio::io_context _ioContext;
|
||||
std::unique_ptr<std::thread> _thread;
|
||||
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ FileResourceHandler::FileResourceHandler(const std::filesystem::path& path)
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Wt::Http::ResponseContinuation*
|
||||
FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
::uint64_t startByte {_offset};
|
||||
@@ -49,7 +49,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
||||
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
|
||||
response.setStatus(404);
|
||||
_isFinished = true;
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -72,7 +72,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
||||
|
||||
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
|
||||
_isFinished = true;
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (ranges.size() == 1)
|
||||
@@ -102,7 +102,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
||||
{
|
||||
LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
|
||||
_isFinished = true;
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
ifs.seekg(static_cast<std::istream::pos_type>(startByte));
|
||||
@@ -123,20 +123,16 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
||||
if (ifs.good() && actualPieceSize < restSize)
|
||||
{
|
||||
_offset = startByte + actualPieceSize;
|
||||
|
||||
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isFinished = true;
|
||||
LMS_LOG(UTILS, DEBUG) << "Job complete!";
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
FileResourceHandler::isFinished() const
|
||||
{
|
||||
return _isFinished;
|
||||
return response.createContinuation();
|
||||
}
|
||||
|
||||
_isFinished = true;
|
||||
LMS_LOG(UTILS, DEBUG) << "Job complete!";
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,10 +29,9 @@ class FileResourceHandler final : public IResourceHandler
|
||||
|
||||
private:
|
||||
|
||||
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
bool isFinished() const override;
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
|
||||
static constexpr std::size_t _chunkSize {262144};
|
||||
static constexpr std::size_t _chunkSize {65536};
|
||||
|
||||
std::filesystem::path _path;
|
||||
::uint64_t _beyondLastByte {};
|
||||
|
||||
@@ -24,20 +24,21 @@ const char* getModuleName(Module mod)
|
||||
switch (mod)
|
||||
{
|
||||
case Module::API_SUBSONIC: return "API_SUBSONIC";
|
||||
case Module::AUTH: return "AUTH";
|
||||
case Module::AV: return "AV";
|
||||
case Module::COVER: return "COVER";
|
||||
case Module::DB: return "DB";
|
||||
case Module::AUTH: return "AUTH";
|
||||
case Module::AV: return "AV";
|
||||
case Module::CHILDPROCESS: return "CHILDPROC";
|
||||
case Module::COVER: return "COVER";
|
||||
case Module::DB: return "DB";
|
||||
case Module::DBUPDATER: return "DB UPDATER";
|
||||
case Module::FEATURE: return "FEATURE";
|
||||
case Module::MAIN: return "MAIN";
|
||||
case Module::MAIN: return "MAIN";
|
||||
case Module::METADATA: return "METADATA";
|
||||
case Module::REMOTE: return "REMOTE";
|
||||
case Module::SERVICE: return "SERVICE";
|
||||
case Module::RECOMMENDATION: return "RECOMMENDATION";
|
||||
case Module::TRANSCODE: return "TRANSCODE";
|
||||
case Module::UI: return "UI";
|
||||
case Module::UTILS: return "UTILS";
|
||||
case Module::UI: return "UI";
|
||||
case Module::UTILS: return "UTILS";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -22,23 +22,21 @@
|
||||
namespace std
|
||||
{
|
||||
std::size_t hash<boost::asio::ip::address>::operator()(const boost::asio::ip::address& ipAddr) const
|
||||
{
|
||||
if (ipAddr.is_v4())
|
||||
return ipAddr.to_v4().to_ulong();
|
||||
|
||||
if (ipAddr.is_v6())
|
||||
{
|
||||
if (ipAddr.is_v4())
|
||||
return ipAddr.to_v4().to_ulong();
|
||||
const auto& range {ipAddr.to_v6().to_bytes()};
|
||||
std::size_t res {};
|
||||
|
||||
if (ipAddr.is_v6())
|
||||
{
|
||||
const auto& range {ipAddr.to_v6().to_bytes()};
|
||||
std::size_t res {};
|
||||
for (auto b : range)
|
||||
res ^= std::hash<char>{}(static_cast<char>(b));
|
||||
|
||||
for (auto b : range)
|
||||
res ^= std::hash<char>{}(static_cast<char>(b));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return std::hash<std::string>{}(ipAddr.to_string());
|
||||
return res;
|
||||
}
|
||||
|
||||
return std::hash<std::string>{}(ipAddr.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
class ChildProcessException : public LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
class IChildProcess
|
||||
{
|
||||
public:
|
||||
using Args = std::vector<std::string>;
|
||||
|
||||
virtual ~IChildProcess() = default;
|
||||
|
||||
enum class ReadResult
|
||||
{
|
||||
Success,
|
||||
Error,
|
||||
EndOfFile,
|
||||
};
|
||||
|
||||
using ReadCallback = std::function<void(ReadResult, std::size_t)>;
|
||||
virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0;
|
||||
|
||||
using WaitCallback = std::function<void(void)>;
|
||||
virtual void asyncWaitForData(WaitCallback cb) = 0;
|
||||
virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0;
|
||||
virtual bool finished() = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <filesystem>
|
||||
#include <memory>
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "IChildProcess.hpp"
|
||||
|
||||
class IChildProcessManager
|
||||
{
|
||||
public:
|
||||
virtual ~IChildProcessManager() = default;
|
||||
|
||||
virtual std::unique_ptr<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IChildProcessManager> createChildProcessManager();
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
#include <Wt/Http/Request.h>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
// Helper class to serve a resource (must be saved as continuation data if not complete)
|
||||
// Helper class to serve a resource (must be saved as continuation data if not complete)
|
||||
class IResourceHandler
|
||||
{
|
||||
public:
|
||||
virtual ~IResourceHandler() = default;
|
||||
|
||||
virtual void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0;
|
||||
virtual bool isFinished() const = 0;
|
||||
[[nodiscard]] virtual Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ enum class Module
|
||||
API_SUBSONIC,
|
||||
AUTH,
|
||||
AV,
|
||||
CHILDPROCESS,
|
||||
COVER,
|
||||
DB,
|
||||
DBUPDATER,
|
||||
|
||||
@@ -32,8 +32,8 @@ add_executable(lms
|
||||
ui/explore/TracksView.cpp
|
||||
ui/resource/AudioFileResource.cpp
|
||||
ui/resource/AudioTranscodeResource.cpp
|
||||
ui/resource/CoverResource.cpp
|
||||
ui/resource/DownloadResource.cpp
|
||||
ui/resource/ImageResource.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lms PRIVATE
|
||||
|
||||
+52
-11
@@ -26,15 +26,14 @@
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "scanner/IMediaScanner.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "subsonic/SubsonicResource.hpp"
|
||||
#include "ui/LmsApplication.hpp"
|
||||
#include "utils/IChildProcessManager.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/WtLogger.hpp"
|
||||
@@ -119,6 +118,47 @@ generateWtConfig(std::string execPath)
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
proxyScannerEventsToApplication(Scanner::IScanner& scanner, Wt::WServer& server)
|
||||
{
|
||||
scanner.getEvents().scanStarted.connect([&]
|
||||
{
|
||||
server.postAll([]
|
||||
{
|
||||
LmsApp->getScannerEvents().scanStarted.emit();
|
||||
LmsApp->triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
scanner.getEvents().scanComplete.connect([&] (const Scanner::ScanStats& stats)
|
||||
{
|
||||
server.postAll([=]
|
||||
{
|
||||
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
||||
LmsApp->triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
scanner.getEvents().scanInProgress.connect([&] (const Scanner::ScanStepStats& stats)
|
||||
{
|
||||
server.postAll([=]
|
||||
{
|
||||
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
||||
LmsApp->triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
scanner.getEvents().scanScheduled.connect([&] (const Wt::WDateTime dateTime)
|
||||
{
|
||||
server.postAll([=]
|
||||
{
|
||||
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
||||
LmsApp->triggerUpdate();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
std::filesystem::path configFilePath {"/etc/lms.conf"};
|
||||
@@ -139,7 +179,6 @@ int main(int argc, char* argv[])
|
||||
|
||||
try
|
||||
{
|
||||
// Make pstream work with ffmpeg
|
||||
close(STDIN_FILENO);
|
||||
|
||||
Service<IConfig> config {createConfig(configFilePath)};
|
||||
@@ -162,9 +201,6 @@ int main(int argc, char* argv[])
|
||||
Wt::WServer server {argv[0]};
|
||||
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
|
||||
|
||||
// lib init
|
||||
Av::Transcoder::init();
|
||||
|
||||
// Initializing a connection pool to the database that will be shared along services
|
||||
Database::Db database {config->getPath("working-dir") / "lms.db"};
|
||||
{
|
||||
@@ -176,6 +212,7 @@ int main(int argc, char* argv[])
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
// Service initialization order is important
|
||||
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager()};
|
||||
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],
|
||||
@@ -184,9 +221,9 @@ int main(int argc, char* argv[])
|
||||
config->getULong("cover-max-file-size", 10) * 1000 * 1000,
|
||||
config->getULong("cover-jpeg-quality", 75))};
|
||||
Service<Recommendation::IEngine> recommendationEngineService {Recommendation::createEngine(database)};
|
||||
Service<Scanner::IMediaScanner> mediaScannerService {Scanner::createMediaScanner(database, *recommendationEngineService)};
|
||||
Service<Scanner::IScanner> scannerService {Scanner::createScanner(database, *recommendationEngineService)};
|
||||
|
||||
mediaScannerService->scanComplete().connect([&]()
|
||||
scannerService->getEvents().scanComplete.connect([&]
|
||||
{
|
||||
// Flush cover cache even if no changes:
|
||||
// covers may be external files that changed and we don't keep track of them
|
||||
@@ -201,8 +238,12 @@ int main(int argc, char* argv[])
|
||||
|
||||
// bind UI entry point
|
||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||
std::bind(UserInterface::LmsApplication::create,
|
||||
std::placeholders::_1, std::ref(database), std::ref(appGroups)));
|
||||
[&](const Wt::WEnvironment &env)
|
||||
{
|
||||
return UserInterface::LmsApplication::create(env, database, appGroups);
|
||||
});
|
||||
|
||||
proxyScannerEventsToApplication(*scannerService, server);
|
||||
|
||||
LMS_LOG(MAIN, INFO) << "Starting server...";
|
||||
server.start();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WEnvironment.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
@@ -34,8 +35,6 @@
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "explore/Explore.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -44,10 +43,12 @@
|
||||
#include "admin/DatabaseSettingsView.hpp"
|
||||
#include "admin/UserView.hpp"
|
||||
#include "admin/UsersView.hpp"
|
||||
#include "explore/Explore.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
#include "resource/AudioTranscodeResource.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "Auth.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "LmsTheme.hpp"
|
||||
@@ -227,6 +228,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
}
|
||||
}
|
||||
|
||||
LmsApplication::~LmsApplication() = default;
|
||||
|
||||
void
|
||||
LmsApplication::finalize()
|
||||
{
|
||||
@@ -440,9 +443,7 @@ LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth)
|
||||
void
|
||||
LmsApplication::createHome()
|
||||
{
|
||||
_audioFileResource = std::make_shared<AudioFileResource>();
|
||||
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
|
||||
_imageResource = std::make_shared<ImageResource>();
|
||||
_coverResource = std::make_shared<CoverResource>();
|
||||
|
||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||
doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})");
|
||||
@@ -532,59 +533,15 @@ LmsApplication::createHome()
|
||||
_mediaPlayer->loadTrack(trackId, play, replayGain);
|
||||
});
|
||||
|
||||
_playQueue->trackUnselected.connect([this] ()
|
||||
_playQueue->trackUnselected.connect([this]
|
||||
{
|
||||
_mediaPlayer->stop();
|
||||
});
|
||||
|
||||
// Events from MediaScanner
|
||||
if (isUserAdmin())
|
||||
{
|
||||
const std::string sessionId {LmsApp->sessionId()};
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scanStarted().connect(this, [=]
|
||||
_scannerEvents.scanComplete.connect([=] (const Scanner::ScanStats& stats)
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
_events.dbScanStarted.emit();
|
||||
triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scanComplete().connect(this, [=]
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [this]
|
||||
{
|
||||
_events.dbScanned.emit();
|
||||
triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats)
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
_events.dbScanInProgress.emit(stepStats);
|
||||
triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->scheduled().connect(this, [=] (Wt::WDateTime dateTime)
|
||||
{
|
||||
Wt::WServer::instance()->post(sessionId, [=]
|
||||
{
|
||||
_events.dbScanScheduled.emit(dateTime);
|
||||
triggerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
_events.dbScanned.connect([=] ()
|
||||
{
|
||||
if (isUserAdmin())
|
||||
{
|
||||
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()))
|
||||
.arg(static_cast<unsigned>(stats.additions))
|
||||
@@ -592,8 +549,8 @@ LmsApplication::createHome()
|
||||
.arg(static_cast<unsigned>(stats.deletions))
|
||||
.arg(static_cast<unsigned>(stats.duplicates.size()))
|
||||
.arg(static_cast<unsigned>(stats.errors.size())));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Events from Application group
|
||||
_events.appOpen.connect([=]
|
||||
@@ -632,14 +589,14 @@ LmsApplication::notify(const Wt::WEvent& event)
|
||||
}
|
||||
}
|
||||
|
||||
static std::string msgTypeToString(MsgType type)
|
||||
static std::string msgTypeToString(LmsApplication::MsgType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case MsgType::Success: return "success";
|
||||
case MsgType::Info: return "info";
|
||||
case MsgType::Warning: return "warning";
|
||||
case MsgType::Danger: return "danger";
|
||||
case LmsApplication::MsgType::Success: return "success";
|
||||
case LmsApplication::MsgType::Info: return "info";
|
||||
case LmsApplication::MsgType::Warning: return "warning";
|
||||
case LmsApplication::MsgType::Danger: return "danger";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -22,13 +22,18 @@
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
|
||||
#include "scanner/IMediaScanner.hpp"
|
||||
#include "scanner/ScannerEvents.hpp"
|
||||
|
||||
#include "LmsApplicationGroup.hpp"
|
||||
|
||||
namespace Database {
|
||||
namespace Wt
|
||||
{
|
||||
class WPopupMenu;
|
||||
}
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class Db;
|
||||
@@ -39,10 +44,8 @@ namespace Database {
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioTranscodeResource;
|
||||
class AudioFileResource;
|
||||
class Auth;
|
||||
class ImageResource;
|
||||
class CoverResource;
|
||||
class LmsApplicationException;
|
||||
class MediaPlayer;
|
||||
class PlayQueue;
|
||||
@@ -53,35 +56,19 @@ struct Events
|
||||
// Events relative to group
|
||||
Wt::Signal<LmsApplicationInfo> appOpen;
|
||||
Wt::Signal<LmsApplicationInfo> appClosed;
|
||||
|
||||
// Database events
|
||||
Wt::Signal<> dbScanStarted;
|
||||
Wt::Signal<> dbScanned;
|
||||
Wt::Signal<Scanner::ScanStepStats> dbScanInProgress;
|
||||
Wt::Signal<Wt::WDateTime> dbScanScheduled;
|
||||
};
|
||||
|
||||
// Used to classify the message sent to the user
|
||||
enum class MsgType
|
||||
{
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Danger,
|
||||
};
|
||||
|
||||
class LmsApplication : public Wt::WApplication
|
||||
{
|
||||
public:
|
||||
LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
|
||||
~LmsApplication();
|
||||
|
||||
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
|
||||
static LmsApplication* instance();
|
||||
|
||||
// Session application data
|
||||
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
|
||||
std::shared_ptr<AudioTranscodeResource> getAudioTranscodeResource() { return _audioTranscodeResource; }
|
||||
std::shared_ptr<AudioFileResource> getAudioFileResource() { return _audioFileResource; }
|
||||
std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; }
|
||||
Database::Session& getDbSession(); // always thread safe
|
||||
|
||||
Wt::Dbo::ptr<Database::User> getUser();
|
||||
@@ -91,10 +78,20 @@ class LmsApplication : public Wt::WApplication
|
||||
std::string getUserLoginName(); // user must be logged in prior this call
|
||||
|
||||
Events& getEvents() { return _events; }
|
||||
Scanner::Events& getScannerEvents() { return _scannerEvents; }
|
||||
|
||||
// Utils
|
||||
void post(std::function<void()> func);
|
||||
void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds(4000));
|
||||
|
||||
// Used to classify the message sent to the user
|
||||
enum class MsgType
|
||||
{
|
||||
Success,
|
||||
Info,
|
||||
Warning,
|
||||
Danger,
|
||||
};
|
||||
void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {4000});
|
||||
|
||||
static Wt::WLink createArtistLink(Wt::Dbo::ptr<Database::Artist> artist);
|
||||
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Wt::Dbo::ptr<Database::Artist> artist, bool addText = true);
|
||||
@@ -129,19 +126,18 @@ class LmsApplication : public Wt::WApplication
|
||||
Wt::Signal<> _preQuit;
|
||||
LmsApplicationGroupContainer& _appGroups;
|
||||
Events _events;
|
||||
Scanner::Events _scannerEvents;
|
||||
std::optional<Database::IdType> _userId;
|
||||
std::optional<bool> _userAuthStrong;
|
||||
std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource;
|
||||
std::shared_ptr<AudioFileResource> _audioFileResource;
|
||||
std::shared_ptr<ImageResource> _imageResource;
|
||||
std::shared_ptr<CoverResource> _coverResource;
|
||||
MediaPlayer* _mediaPlayer {};
|
||||
PlayQueue* _playQueue {};
|
||||
std::unique_ptr<Wt::WPopupMenu> _popupMenu {};
|
||||
std::unique_ptr<Wt::WPopupMenu> _popupMenu;
|
||||
};
|
||||
|
||||
|
||||
// Helper to get session instance
|
||||
#define LmsApp LmsApplication::instance()
|
||||
#define LmsApp ::UserInterface::LmsApplication::instance()
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
+13
-10
@@ -33,7 +33,7 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "resource/AudioTranscodeResource.hpp"
|
||||
#include "resource/AudioFileResource.hpp"
|
||||
|
||||
@@ -192,14 +192,17 @@ static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings
|
||||
}
|
||||
|
||||
MediaPlayer::MediaPlayer()
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")},
|
||||
playbackEnded {this, "playbackEnded"},
|
||||
playPrevious {this, "playPrevious"},
|
||||
playNext {this, "playNext"},
|
||||
_settingsLoaded {this, "settingsLoaded"}
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.MediaPlayer.template")}
|
||||
, playbackEnded {this, "playbackEnded"}
|
||||
, playPrevious {this, "playPrevious"}
|
||||
, playNext {this, "playNext"}
|
||||
, _settingsLoaded {this, "settingsLoaded"}
|
||||
{
|
||||
addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
_audioTranscodeResource = std::make_unique<AudioTranscodeResource>();
|
||||
_audioFileResource = std::make_unique<AudioFileResource>();
|
||||
|
||||
_title = bindNew<Wt::WText>("title");
|
||||
_artist = bindNew<Wt::WAnchor>("artist");
|
||||
_release = bindNew<Wt::WAnchor>("release");
|
||||
@@ -240,8 +243,8 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
const std::string transcodeResource {LmsApp->getAudioTranscodeResource()->getUrl(trackId)};
|
||||
const std::string nativeResource {LmsApp->getAudioFileResource()->getUrl(trackId)};
|
||||
const std::string transcodeResource {_audioTranscodeResource->getUrl(trackId)};
|
||||
const std::string nativeResource {_audioFileResource->getUrl(trackId)};
|
||||
|
||||
const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
|
||||
@@ -255,8 +258,8 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play, float replayGain)
|
||||
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
|
||||
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||
<< " artwork: ["
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
||||
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
||||
<< " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
||||
<< " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
||||
<< " ]"
|
||||
<< "};";
|
||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class AudioFileResource;
|
||||
class AudioTranscodeResource;
|
||||
|
||||
class MediaPlayer : public Wt::WTemplate
|
||||
{
|
||||
public:
|
||||
@@ -105,6 +108,8 @@ class MediaPlayer : public Wt::WTemplate
|
||||
Wt::Signal<> settingsLoaded;
|
||||
|
||||
private:
|
||||
std::unique_ptr<AudioFileResource> _audioFileResource;
|
||||
std::unique_ptr<AudioTranscodeResource> _audioTranscodeResource;
|
||||
|
||||
std::optional<Database::IdType> _trackIdLoaded;
|
||||
std::optional<Settings> _settings;
|
||||
|
||||
+122
-84
@@ -19,7 +19,7 @@
|
||||
|
||||
#include "PlayQueue.hpp"
|
||||
|
||||
#include <Wt/WText.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
@@ -34,7 +34,8 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/LoadingIndicator.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackStringUtils.hpp"
|
||||
@@ -394,10 +395,10 @@ PlayQueue::processTracks(PlayQueueAction action, const std::vector<Database::IdT
|
||||
}
|
||||
|
||||
if (nbAddedTracks > 0)
|
||||
LmsApp->notifyMsg(MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Info, Wt::WString::trn("Lms.PlayQueue.nb-tracks-added", nbAddedTracks).arg(nbAddedTracks), std::chrono::milliseconds(2000));
|
||||
|
||||
if (isFull())
|
||||
LmsApp->notifyMsg(MsgType::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Warning, Wt::WString::tr("Lms.PlayQueue.playqueue-full"), std::chrono::milliseconds(2000));
|
||||
|
||||
|
||||
}
|
||||
@@ -411,86 +412,7 @@ PlayQueue::addSome()
|
||||
|
||||
auto tracklistEntries = tracklist->getEntries(_entriesContainer->count(), 50);
|
||||
for (const Database::TrackListEntry::pointer& tracklistEntry : tracklistEntries)
|
||||
{
|
||||
const auto tracklistEntryId {tracklistEntry.id()};
|
||||
const auto track {tracklistEntry->getTrack()};
|
||||
|
||||
Wt::WTemplate* entry = _entriesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry"));
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
const auto release {track->getRelease()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
if (release)
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), ImageResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(track.id(), ImageResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
}
|
||||
|
||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
playBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
auto pos = _entriesContainer->indexOf(entry);
|
||||
if (pos >= 0)
|
||||
loadTrack(pos, true);
|
||||
}));
|
||||
|
||||
Wt::WText* delBtn = entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.template.delete-btn"), Wt::TextFormat::XHTML);
|
||||
delBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
// Remove the entry n both the widget tree and the playqueue
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
Database::TrackListEntry::pointer entryToRemove {Database::TrackListEntry::getById(LmsApp->getDbSession(), tracklistEntryId)};
|
||||
entryToRemove.remove();
|
||||
}
|
||||
|
||||
if (_trackPos)
|
||||
{
|
||||
auto pos {_entriesContainer->indexOf(entry)};
|
||||
if (pos > 0 && *_trackPos >= static_cast<std::size_t>(pos))
|
||||
(*_trackPos)--;
|
||||
}
|
||||
|
||||
_entriesContainer->removeWidget(entry);
|
||||
|
||||
updateInfo();
|
||||
}));
|
||||
|
||||
}
|
||||
addEntry(tracklistEntry);
|
||||
|
||||
if (static_cast<std::size_t>(_entriesContainer->count()) < tracklist->getCount())
|
||||
displayLoadingIndicator();
|
||||
@@ -498,6 +420,122 @@ PlayQueue::addSome()
|
||||
hideLoadingIndicator();
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
{
|
||||
const auto tracklistEntryId {tracklistEntry.id()};
|
||||
const auto track {tracklistEntry->getTrack()};
|
||||
const Database::IdType trackId {track->id()};
|
||||
|
||||
Wt::WTemplate* entry = _entriesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry"));
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
const auto release {track->getRelease()};
|
||||
|
||||
if (!artists.empty() || release)
|
||||
entry->setCondition("if-has-artists-or-release", true);
|
||||
|
||||
if (!artists.empty())
|
||||
{
|
||||
entry->setCondition("if-has-artists", true);
|
||||
|
||||
Wt::WContainerWidget* artistContainer {entry->bindNew<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* a {artistContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry-artist"))};
|
||||
a->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
if (release)
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
anchor->setImage(std::move(cover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(track.id(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
}
|
||||
|
||||
entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
|
||||
Wt::WText* playBtn {entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.template.play-btn"), Wt::TextFormat::XHTML)};
|
||||
playBtn->clicked().connect([=]
|
||||
{
|
||||
auto pos = _entriesContainer->indexOf(entry);
|
||||
if (pos >= 0)
|
||||
loadTrack(pos, true);
|
||||
});
|
||||
|
||||
Wt::WText* delBtn {entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.template.delete-btn"), Wt::TextFormat::XHTML)};
|
||||
delBtn->clicked().connect([=]
|
||||
{
|
||||
// Remove the entry n both the widget tree and the playqueue
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
Database::TrackListEntry::pointer entryToRemove {Database::TrackListEntry::getById(LmsApp->getDbSession(), tracklistEntryId)};
|
||||
entryToRemove.remove();
|
||||
}
|
||||
|
||||
if (_trackPos)
|
||||
{
|
||||
auto pos {_entriesContainer->indexOf(entry)};
|
||||
if (pos > 0 && *_trackPos >= static_cast<std::size_t>(pos))
|
||||
(*_trackPos)--;
|
||||
}
|
||||
|
||||
_entriesContainer->removeWidget(entry);
|
||||
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
Wt::WText* moreBtn {entry->bindNew<Wt::WText>("more-btn", Wt::WString::tr("Lms.PlayQueue.template.more-btn"), Wt::TextFormat::XHTML)};
|
||||
moreBtn->clicked().connect([=]
|
||||
{
|
||||
Wt::WPopupMenu* popup {LmsApp->createPopupMenu()};
|
||||
|
||||
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(moreBtn, [=]
|
||||
{
|
||||
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(moreBtn);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
PlayQueue::enqueueRadioTracks()
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace Similarity {
|
||||
namespace Database {
|
||||
class Track;
|
||||
class TrackList;
|
||||
class TrackListEntry;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
@@ -65,6 +66,7 @@ class PlayQueue : public Wt::WTemplate
|
||||
void clearTracks();
|
||||
std::size_t enqueueTracks(const std::vector<Database::IdType>& trackIds);
|
||||
void addSome();
|
||||
void addEntry(const Wt::Dbo::ptr<Database::TrackListEntry>& entry);
|
||||
void enqueueRadioTracks();
|
||||
void updateInfo();
|
||||
void updateCurrentTrack(bool selected);
|
||||
|
||||
@@ -514,7 +514,7 @@ SettingsView::refreshView()
|
||||
|
||||
if (LmsApp->getUser()->isDemo())
|
||||
{
|
||||
LmsApp->notifyMsg(MsgType::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save"));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ SettingsView::refreshView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
|
||||
}
|
||||
|
||||
// Udate the view: Delete any validation message in the view, etc.
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -47,11 +48,13 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
// Associate each field with a unique string literal.
|
||||
static inline const Field MediaDirectoryField {"media-directory"};
|
||||
static inline const Field UpdatePeriodField {"update-period"};
|
||||
static inline const Field UpdateStartTimeField {"update-start-time"};
|
||||
static inline const Field RecommendationEngineTypeField {"recommendation-engine-type"};
|
||||
static inline const Field TagsField {"tags"};
|
||||
static inline constexpr Field MediaDirectoryField {"media-directory"};
|
||||
static inline constexpr Field UpdatePeriodField {"update-period"};
|
||||
static inline constexpr Field UpdateStartTimeField {"update-start-time"};
|
||||
static inline constexpr Field RecommendationEngineTypeField {"recommendation-engine-type"};
|
||||
static inline constexpr Field TagsField {"tags"};
|
||||
|
||||
using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
|
||||
|
||||
DatabaseSettingsModel()
|
||||
{
|
||||
@@ -76,7 +79,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
loadData();
|
||||
}
|
||||
|
||||
std::shared_ptr<Wt::WAbstractItemModel> updatePeriodModel() { return _updatePeriodModel; }
|
||||
std::shared_ptr<UpdatePeriodModel> updatePeriodModel() { return _updatePeriodModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> updateStartTimeModel() { return _updateStartTimeModel; }
|
||||
std::shared_ptr<Wt::WAbstractItemModel> recommendationEngineTypeModel() { return _recommendationEngineTypeModel; }
|
||||
|
||||
@@ -96,6 +99,12 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
if (startTimeRow)
|
||||
setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
|
||||
|
||||
if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
|
||||
|| scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
|
||||
{
|
||||
setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
|
||||
}
|
||||
|
||||
auto recommendationEngineTypeRow {_recommendationEngineTypeModel->getRowFromValue(scanSettings->getRecommendationEngineType())};
|
||||
if (recommendationEngineTypeRow)
|
||||
setValue(RecommendationEngineTypeField, _recommendationEngineTypeModel->getString(*recommendationEngineTypeRow));
|
||||
@@ -144,6 +153,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
{
|
||||
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly);
|
||||
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly);
|
||||
@@ -160,7 +170,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.features"), ScanSettings::RecommendationEngineType::Features);
|
||||
}
|
||||
|
||||
std::shared_ptr<ValueStringModel<ScanSettings::UpdatePeriod>> _updatePeriodModel;
|
||||
std::shared_ptr<UpdatePeriodModel> _updatePeriodModel;
|
||||
std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel;
|
||||
std::shared_ptr<ValueStringModel<ScanSettings::RecommendationEngineType>> _recommendationEngineTypeModel;
|
||||
};
|
||||
@@ -192,6 +202,13 @@ DatabaseSettingsView::refreshView()
|
||||
// Update Period
|
||||
auto updatePeriod {std::make_unique<Wt::WComboBox>()};
|
||||
updatePeriod->setModel(model->updatePeriodModel());
|
||||
updatePeriod->activated().connect([=](int row)
|
||||
{
|
||||
const ScanSettings::UpdatePeriod period {model->updatePeriodModel()->getValue(row)};
|
||||
model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never);
|
||||
t->updateModel(model.get());
|
||||
t->updateView(model.get());
|
||||
});
|
||||
t->setFormWidget(DatabaseSettingsModel::UpdatePeriodField, std::move(updatePeriod));
|
||||
|
||||
// Update Start Time
|
||||
@@ -222,8 +239,8 @@ DatabaseSettingsView::refreshView()
|
||||
{
|
||||
model->saveData();
|
||||
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||
Service<Scanner::IScanner>::get()->requestImmediateScan(false);
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
|
||||
}
|
||||
|
||||
// Udate the view: Delete any validation message in the view, etc.
|
||||
@@ -239,7 +256,7 @@ DatabaseSettingsView::refreshView()
|
||||
|
||||
immScanBtn->clicked().connect([=]
|
||||
{
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<Scanner::IScanner>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
t->updateView(model.get());
|
||||
|
||||
@@ -202,7 +202,7 @@ InitWizardView::InitWizardView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.InitWizard.done"));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.InitWizard.done"));
|
||||
saveButton->setEnabled(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -139,13 +140,13 @@ ScannerController::ScannerController()
|
||||
|
||||
auto onDbEvent = [&]() { refreshContents(); };
|
||||
|
||||
LmsApp->getEvents().dbScanStarted.connect(this, []
|
||||
LmsApp->getScannerEvents().scanStarted.connect(this, []
|
||||
{
|
||||
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
|
||||
});
|
||||
LmsApp->getEvents().dbScanned.connect(this, onDbEvent);
|
||||
LmsApp->getEvents().dbScanInProgress.connect(this, onDbEvent);
|
||||
LmsApp->getEvents().dbScanScheduled.connect(this, onDbEvent);
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, onDbEvent);
|
||||
LmsApp->getScannerEvents().scanInProgress.connect(this, onDbEvent);
|
||||
LmsApp->getScannerEvents().scanScheduled.connect(this, onDbEvent);
|
||||
|
||||
refreshContents();
|
||||
}
|
||||
@@ -162,20 +163,20 @@ ScannerController::refreshContents()
|
||||
actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now"));
|
||||
actionBtn->actionButton()->clicked().connect([]
|
||||
{
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(false);
|
||||
Service<Scanner::IScanner>::get()->requestImmediateScan(false);
|
||||
});
|
||||
|
||||
auto popup = std::make_unique<Wt::WPopupMenu>();
|
||||
popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now"));
|
||||
popup->itemSelected().connect([]
|
||||
{
|
||||
Service<Scanner::IMediaScanner>::get()->requestImmediateScan(true);
|
||||
Service<Scanner::IScanner>::get()->requestImmediateScan(true);
|
||||
});
|
||||
actionBtn->dropDownButton()->setMenu(std::move(popup));
|
||||
actionBtn->dropDownButton()->addStyleClass("btn-primary");
|
||||
|
||||
|
||||
const IMediaScanner::Status status {Service<IMediaScanner>::get()->getStatus()};
|
||||
const IScanner::Status status {Service<IScanner>::get()->getStatus()};
|
||||
if (status.lastCompleteScanStats)
|
||||
{
|
||||
bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status")
|
||||
@@ -199,16 +200,16 @@ ScannerController::refreshContents()
|
||||
|
||||
switch (status.currentState)
|
||||
{
|
||||
case IMediaScanner::State::NotScheduled:
|
||||
case IScanner::State::NotScheduled:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-not-scheduled"));
|
||||
bindEmpty("step-status");
|
||||
break;
|
||||
case IMediaScanner::State::Scheduled:
|
||||
case IScanner::State::Scheduled:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-scheduled")
|
||||
.arg(status.nextScheduledScan.toString()));
|
||||
bindEmpty("step-status");
|
||||
break;
|
||||
case IMediaScanner::State::InProgress:
|
||||
case IScanner::State::InProgress:
|
||||
bindString("status", Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
|
||||
.arg(static_cast<int>(status.currentScanStepStats->currentStep) + 1)
|
||||
.arg(Scanner::ScanProgressStepCount));
|
||||
|
||||
@@ -316,7 +316,7 @@ UserView::refreshView()
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr(userId ? "Lms.Admin.User.user-updated" : "Lms.Admin.User.user-created"));
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr(userId ? "Lms.Admin.User.user-updated" : "Lms.Admin.User.user-created"));
|
||||
LmsApp->setInternalPath("/admin/users", true);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
|
||||
@@ -73,9 +73,10 @@ Artists::Artists(Filters* filters)
|
||||
_linkType->changed().connect([this] { refreshView(); });
|
||||
refreshArtistLinkTypes();
|
||||
|
||||
LmsApp->getEvents().dbScanned.connect(this, [this]
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||
{
|
||||
refreshArtistLinkTypes();
|
||||
if (stats.nbChanges())
|
||||
refreshArtistLinkTypes();
|
||||
});
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("artists");
|
||||
|
||||
@@ -150,7 +150,7 @@ Filters::add(Database::IdType clusterId)
|
||||
_sigUpdated.emit();
|
||||
});
|
||||
|
||||
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds {2});
|
||||
LmsApp->notifyMsg(LmsApplication::MsgType::Info, Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds {2});
|
||||
|
||||
_sigUpdated.emit();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace UserInterface::ReleaseListHelpers
|
||||
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), ImageResource::Size::Large));
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "ReleasePopup.hpp"
|
||||
|
||||
#include <Wt/WPopupMenu.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
@@ -127,7 +127,7 @@ Release::refreshView()
|
||||
}
|
||||
|
||||
{
|
||||
Wt::WImage* cover {bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), ImageResource::Size::Large)))};
|
||||
Wt::WImage* cover {bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large)))};
|
||||
cover->setStyleClass("Lms-cover-large");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Release.hpp"
|
||||
@@ -34,7 +35,6 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/LoadingIndicator.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ReleaseListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "resource/DownloadResource.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "TrackPopup.hpp"
|
||||
@@ -71,9 +71,9 @@ namespace UserInterface::TrackListHelpers
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
{
|
||||
Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false));
|
||||
auto cover = std::make_unique<Wt::WImage>();
|
||||
cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), ImageResource::Size::Large));
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
|
||||
auto cover {std::make_unique<Wt::WImage>()};
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(release.id(), CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
anchor->setImage(std::move(cover));
|
||||
@@ -81,8 +81,8 @@ namespace UserInterface::TrackListHelpers
|
||||
}
|
||||
else
|
||||
{
|
||||
auto cover = entry->bindNew<Wt::WImage>("cover");
|
||||
cover->setImageLink(LmsApp->getImageResource()->getTrackUrl(trackId, ImageResource::Size::Large));
|
||||
auto* cover {entry->bindNew<Wt::WImage>("cover")};
|
||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Large));
|
||||
cover->setStyleClass("Lms-cover");
|
||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "TrackPopup.hpp"
|
||||
|
||||
#include <Wt/WPopupMenu.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
#include "TracksView.hpp"
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WMenu.h>
|
||||
#include <Wt/WPopupMenu.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
@@ -34,7 +35,6 @@
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/LoadingIndicator.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <fstream>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/FileResourceHandlerCreator.hpp"
|
||||
@@ -101,13 +101,9 @@ AudioFileResource::handleRequest(const Wt::Http::Request& request,
|
||||
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
|
||||
}
|
||||
|
||||
fileResourceHandler->processRequest(request, response);
|
||||
|
||||
if (!fileResourceHandler->isFinished())
|
||||
{
|
||||
auto* continuation {response.createContinuation()};
|
||||
auto* continuation {fileResourceHandler->processRequest(request, response)};
|
||||
if (continuation)
|
||||
continuation->setData(fileResourceHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
|
||||
#include "AudioTranscodeResource.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||
#include "av/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
@@ -66,20 +69,13 @@ namespace StringUtils
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
AudioTranscodeResource:: ~AudioTranscodeResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<Av::Format>
|
||||
AudioFormatToAvFormat(Database::AudioFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case Database::AudioFormat::MP3: return Av::Format::MP3;
|
||||
case Database::AudioFormat::MP3: return Av::Format::MP3;
|
||||
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||
@@ -91,6 +87,14 @@ AudioFormatToAvFormat(Database::AudioFormat format)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
AudioTranscodeResource:: ~AudioTranscodeResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
std::string
|
||||
AudioTranscodeResource::getUrl(Database::IdType trackId) const
|
||||
{
|
||||
@@ -115,102 +119,85 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa
|
||||
return res;
|
||||
}
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
std::filesystem::path file;
|
||||
Av::TranscodeParameters transcodeParameters;
|
||||
};
|
||||
|
||||
static
|
||||
std::optional<TranscodeParameters>
|
||||
readTranscodeParameters(const Wt::Http::Request& request)
|
||||
{
|
||||
TranscodeParameters parameters;
|
||||
|
||||
// mandatory parameters
|
||||
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
|
||||
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return std::nullopt;
|
||||
|
||||
// optional parameter
|
||||
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
parameters.file = track->getPath();
|
||||
|
||||
if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates))
|
||||
{
|
||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
parameters.transcodeParameters.stripMetadata = true;
|
||||
parameters.transcodeParameters.format = *avFormat;
|
||||
parameters.transcodeParameters.bitrate = *bitrate;
|
||||
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
void
|
||||
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
|
||||
Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<Av::Transcoder> transcoder;
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
// First, see if this request is for a continuation
|
||||
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||
if (continuation)
|
||||
Wt::Http::ResponseContinuation* continuation {request.continuation()};
|
||||
if (!continuation)
|
||||
{
|
||||
LOG(DEBUG) << "Continuation! " << continuation ;
|
||||
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
|
||||
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
|
||||
if (parameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG) << "First request: creating transcoder";
|
||||
|
||||
// mandatory parameters
|
||||
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
|
||||
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return;
|
||||
|
||||
auto avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return;
|
||||
|
||||
// optional parameter
|
||||
auto offset {readParameterAs<std::size_t>(request, "offset")};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)};
|
||||
if (!track)
|
||||
{
|
||||
LOG(ERROR) << "Missing track";
|
||||
return;
|
||||
}
|
||||
|
||||
trackPath = track->getPath();
|
||||
|
||||
if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates))
|
||||
{
|
||||
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Av::TranscodeParameters parameters {};
|
||||
parameters.stripMetadata = true;
|
||||
parameters.format = *avFormat;
|
||||
parameters.bitrate = *bitrate;
|
||||
parameters.offset = std::chrono::seconds {offset ? *offset : 0};
|
||||
|
||||
transcoder = std::make_shared<Av::Transcoder>(trackPath, parameters);
|
||||
if (!transcoder->start())
|
||||
{
|
||||
LOG(ERROR) << "Cannot start transcoder";
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(DEBUG) << "Transcoder started";
|
||||
|
||||
std::string mimeType {transcoder->getOutputMimeType()};
|
||||
response.setMimeType(mimeType);
|
||||
LOG(DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
if (resourceHandler)
|
||||
{
|
||||
std::vector<unsigned char> data;
|
||||
data.reserve(_chunkSize);
|
||||
|
||||
transcoder->process(data, _chunkSize);
|
||||
|
||||
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||
LOG(DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
|
||||
|
||||
if (!response.out())
|
||||
{
|
||||
LOG(ERROR) << "Write failed!";
|
||||
}
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete() && response.out())
|
||||
{
|
||||
continuation = response.createContinuation();
|
||||
continuation->setData(transcoder);
|
||||
}
|
||||
else
|
||||
LOG(DEBUG) << "No more data!";
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ImageResource.hpp"
|
||||
#include "CoverResource.hpp"
|
||||
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/Http/Response.h>
|
||||
@@ -35,25 +35,34 @@
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
ImageResource::~ImageResource()
|
||||
CoverResource::CoverResource()
|
||||
{
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||
{
|
||||
if (stats.nbChanges())
|
||||
setChanged();
|
||||
});
|
||||
}
|
||||
|
||||
CoverResource::~CoverResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
std::string
|
||||
ImageResource::getReleaseUrl(Database::IdType releaseId, Size size) const
|
||||
CoverResource::getReleaseUrl(Database::IdType releaseId, Size size) const
|
||||
{
|
||||
return url() + "&releaseid=" + std::to_string(releaseId) + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
std::string
|
||||
ImageResource::getTrackUrl(Database::IdType trackId, Size size) const
|
||||
CoverResource::getTrackUrl(Database::IdType trackId, Size size) const
|
||||
{
|
||||
return url() + "&trackid=" + std::to_string(trackId) + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
||||
}
|
||||
|
||||
void
|
||||
ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
// Retrieve parameters
|
||||
const std::string *trackIdStr = request.getParameter("trackid");
|
||||
@@ -25,12 +25,13 @@
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
class ImageResource : public Wt::WResource
|
||||
class CoverResource : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
static const std::size_t maxSize {512};
|
||||
|
||||
~ImageResource();
|
||||
CoverResource();
|
||||
~CoverResource();
|
||||
|
||||
enum class Size : std::size_t
|
||||
{
|
||||
@@ -43,7 +44,6 @@ namespace UserInterface
|
||||
|
||||
private:
|
||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
|
||||
};
|
||||
|
||||
} // namespace UserInterface
|
||||
Reference in New Issue
Block a user