Merge branch 'develop' for release 3.9.0

This commit is contained in:
emeric
2020-03-30 22:44:03 +02:00
33 changed files with 506 additions and 210 deletions
+4 -4
View File
@@ -9,17 +9,17 @@ matrix:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-8
- g++-9
env:
- MATRIX_EVAL="CC=gcc-8 && CXX=g++-8"
- MATRIX_EVAL="CC=gcc-9 && CXX=g++-9"
compiler:
- gcc
before_install:
- eval "${MATRIX_EVAL}"
- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libmagick++-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev
- sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libgraphicsmagick++1-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev
- git clone https://github.com/emweb/wt.git wt
- pushd wt;
- git checkout 4.1.0
- git checkout 4.2.1
- cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && sudo make install
- popd
script: cmake -DCMAKE_BUILD_TYPE=Release . && make && make test
+6 -2
View File
@@ -2,12 +2,16 @@ cmake_minimum_required(VERSION 3.10)
project(lms)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
include(CTest)
find_package(PkgConfig)
pkg_check_modules(IMAGEMAGICKXX REQUIRED ImageMagick++)
find_package(PkgConfig REQUIRED)
find_package(Filesystem REQUIRED)
pkg_check_modules(GRAPHICSMAGICKXX REQUIRED GraphicsMagick++)
add_subdirectory(src)
+3 -3
View File
@@ -85,7 +85,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 libmagick++-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libgraphicsmagick++1-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev
```
You also need _Wt4_, which is not packaged yet on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).</br>
@@ -157,7 +157,7 @@ systemctl restart lms
__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan.
### Configuration
_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, ...).
_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...).
All other settings are set using the web interface (user management, scan settings, transcode settings, ...).
@@ -208,6 +208,6 @@ To connect to _LMS_, just open your favorite browser and go to http://localhost:
* Wt (http://www.webtoolkit.eu/)
* bootstrap3 (http://getbootstrap.com/)
* ffmpeg project (https://ffmpeg.org/)
* Magick++ (http://www.imagemagick.org/Magick++/)
* GraphicsMagick++ (http://www.graphicsmagick.org/)
* MetaBrainz (https://metabrainz.org/)
* Bootstrap Notify: https://github.com/mouse0270/bootstrap-notify
+233
View File
@@ -0,0 +1,233 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
#[=======================================================================[.rst:
FindFilesystem
##############
This module supports the C++17 standard library's filesystem utilities. Use the
:imp-target:`std::filesystem` imported target to
Options
*******
The ``COMPONENTS`` argument to this module supports the following values:
.. find-component:: Experimental
:name: fs.Experimental
Allows the module to find the "experimental" Filesystem TS version of the
Filesystem library. This is the library that should be used with the
``std::experimental::filesystem`` namespace.
.. find-component:: Final
:name: fs.Final
Finds the final C++17 standard version of the filesystem library.
If no components are provided, behaves as if the
:find-component:`fs.Final` component was specified.
If both :find-component:`fs.Experimental` and :find-component:`fs.Final` are
provided, first looks for ``Final``, and falls back to ``Experimental`` in case
of failure. If ``Final`` is found, :imp-target:`std::filesystem` and all
:ref:`variables <fs.variables>` will refer to the ``Final`` version.
Imported Targets
****************
.. imp-target:: std::filesystem
The ``std::filesystem`` imported target is defined when any requested
version of the C++ filesystem library has been found, whether it is
*Experimental* or *Final*.
If no version of the filesystem library is available, this target will not
be defined.
.. note::
This target has ``cxx_std_17`` as an ``INTERFACE``
:ref:`compile language standard feature <req-lang-standards>`. Linking
to this target will automatically enable C++17 if no later standard
version is already required on the linking target.
.. _fs.variables:
Variables
*********
.. variable:: CXX_FILESYSTEM_IS_EXPERIMENTAL
Set to ``TRUE`` when the :find-component:`fs.Experimental` version of C++
filesystem library was found, otherwise ``FALSE``.
.. variable:: CXX_FILESYSTEM_HAVE_FS
Set to ``TRUE`` when a filesystem header was found.
.. variable:: CXX_FILESYSTEM_HEADER
Set to either ``filesystem`` or ``experimental/filesystem`` depending on
whether :find-component:`fs.Final` or :find-component:`fs.Experimental` was
found.
.. variable:: CXX_FILESYSTEM_NAMESPACE
Set to either ``std::filesystem`` or ``std::experimental::filesystem``
depending on whether :find-component:`fs.Final` or
:find-component:`fs.Experimental` was found.
Examples
********
Using `find_package(Filesystem)` with no component arguments:
.. code-block:: cmake
find_package(Filesystem REQUIRED)
add_executable(my-program main.cpp)
target_link_libraries(my-program PRIVATE std::filesystem)
#]=======================================================================]
if(TARGET std::filesystem)
# This module has already been processed. Don't do it again.
return()
endif()
include(CMakePushCheckState)
include(CheckIncludeFileCXX)
include(CheckCXXSourceCompiles)
cmake_push_check_state()
set(CMAKE_REQUIRED_QUIET ${Filesystem_FIND_QUIETLY})
# All of our tests required C++17 or later
set(CMAKE_CXX_STANDARD 17)
# Normalize and check the component list we were given
set(want_components ${Filesystem_FIND_COMPONENTS})
if(Filesystem_FIND_COMPONENTS STREQUAL "")
set(want_components Final)
endif()
# Warn on any unrecognized components
set(extra_components ${want_components})
list(REMOVE_ITEM extra_components Final Experimental)
foreach(component IN LISTS extra_components)
message(WARNING "Extraneous find_package component for Filesystem: ${component}")
endforeach()
# Detect which of Experimental and Final we should look for
set(find_experimental TRUE)
set(find_final TRUE)
if(NOT "Final" IN_LIST want_components)
set(find_final FALSE)
endif()
if(NOT "Experimental" IN_LIST want_components)
set(find_experimental FALSE)
endif()
if(find_final)
check_include_file_cxx("filesystem" _CXX_FILESYSTEM_HAVE_HEADER)
mark_as_advanced(_CXX_FILESYSTEM_HAVE_HEADER)
if(_CXX_FILESYSTEM_HAVE_HEADER)
# We found the non-experimental header. Don't bother looking for the
# experimental one.
set(find_experimental FALSE)
endif()
else()
set(_CXX_FILESYSTEM_HAVE_HEADER FALSE)
endif()
if(find_experimental)
check_include_file_cxx("experimental/filesystem" _CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
mark_as_advanced(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
else()
set(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER FALSE)
endif()
if(_CXX_FILESYSTEM_HAVE_HEADER)
set(_have_fs TRUE)
set(_fs_header filesystem)
set(_fs_namespace std::filesystem)
elseif(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
set(_have_fs TRUE)
set(_fs_header experimental/filesystem)
set(_fs_namespace std::experimental::filesystem)
else()
set(_have_fs FALSE)
endif()
set(CXX_FILESYSTEM_HAVE_FS ${_have_fs} CACHE BOOL "TRUE if we have the C++ filesystem headers")
set(CXX_FILESYSTEM_HEADER ${_fs_header} CACHE STRING "The header that should be included to obtain the filesystem APIs")
set(CXX_FILESYSTEM_NAMESPACE ${_fs_namespace} CACHE STRING "The C++ namespace that contains the filesystem APIs")
set(_found FALSE)
if(CXX_FILESYSTEM_HAVE_FS)
# We have some filesystem library available. Do link checks
string(CONFIGURE [[
#include <@CXX_FILESYSTEM_HEADER@>
int main() {
auto cwd = @CXX_FILESYSTEM_NAMESPACE@::current_path();
return static_cast<int>(cwd.string().size());
}
]] code @ONLY)
# Try to compile a simple filesystem program without any linker flags
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED)
set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED})
if(NOT CXX_FILESYSTEM_NO_LINK_NEEDED)
set(prev_libraries ${CMAKE_REQUIRED_LIBRARIES})
# Try to compile a simple filesystem program with the libstdc++ flag
set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lstdc++fs)
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_STDCPPFS_NEEDED)
set(can_link ${CXX_FILESYSTEM_STDCPPFS_NEEDED})
if(NOT CXX_FILESYSTEM_STDCPPFS_NEEDED)
# Try to compile a simple filesystem program with the libc++ flag
set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lc++fs)
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_CPPFS_NEEDED)
set(can_link ${CXX_FILESYSTEM_CPPFS_NEEDED})
if(NOT CXX_FILESYSTEM_CPPFS_NEEDED)
# Try to compile a simple filesystem program without any linker flags
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED)
set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED})
endif()
endif()
endif()
if(can_link)
add_library(std::filesystem INTERFACE IMPORTED)
target_compile_features(std::filesystem INTERFACE cxx_std_17)
set(_found TRUE)
if(CXX_FILESYSTEM_NO_LINK_NEEDED)
# Nothing to add...
elseif(CXX_FILESYSTEM_STDCPPFS_NEEDED)
target_link_libraries(std::filesystem INTERFACE -lstdc++fs)
elseif(CXX_FILESYSTEM_CPPFS_NEEDED)
target_link_libraries(std::filesystem INTERFACE -lc++fs)
endif()
endif()
endif()
cmake_pop_check_state()
set(Filesystem_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::filesystem" FORCE)
if(Filesystem_FIND_REQUIRED AND NOT Filesystem_FOUND)
message(FATAL_ERROR "Cannot Compile simple program using std::filesystem")
endif()
+3
View File
@@ -18,6 +18,9 @@ listen-port = 5082;
listen-addr = "0.0.0.0";
behind-reverse-proxy = false;
# Location for deployment
deploy-path = "/";
# If enabled, these files have to exist and have correct permissions
tls-enable = false;
tls-cert = "/var/lms/cert.pem";
+37 -41
View File
@@ -5,7 +5,7 @@ WORKDIR /tmp/workdir
ARG MAKEFLAGS="-j2"
ARG FFMPEG_VERSION=4.1.4
ARG WT_VERSION=4.2.0
ARG IMAGEMAGICK6_VERSION=6.9.10-71
ARG GRAPHICSMAGICK_VERSION=1.3.35
ARG PSTREAMS_VERSION=1.0.1
ARG LMS_VERSION=v3.6.3
@@ -24,6 +24,7 @@ ARG BUILD_PACKAGES=" \
make \
libjpeg-turbo-dev \
openjpeg-dev \
jasper-dev \
libpng-dev \
tiff-dev \
nasm \
@@ -95,50 +96,42 @@ RUN \
make && \
make install
# ImageMagick
# GraphicsMagick++
RUN \
DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://github.com/ImageMagick/ImageMagick6/archive/${IMAGEMAGICK6_VERSION}.tar.gz && \
tar -x --strip-components=1 -f ${IMAGEMAGICK6_VERSION}.tar.gz
DIR=/tmp/graphicsmagick && mkdir -p ${DIR} && cd ${DIR} && \
curl -sL ftp://ftp.graphicsmagick.org/pub/GraphicsMagick/1.3/GraphicsMagick-${GRAPHICSMAGICK_VERSION}.tar.gz -o ${GRAPHICSMAGICK_VERSION}.tar.gz && \
tar -x --strip-components=1 -f ${GRAPHICSMAGICK_VERSION}.tar.gz
RUN \
DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \
DIR=/tmp/graphicsmagick && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--prefix=${PREFIX} \
--enable-shared=yes \
--disable-static \
--disable-docs \
--with-magick-plus-plus \
--without-zstd \
--without-dps \
--without-autotrace \
--without-dps \
--without-fftw \
--without-flif \
--without-fpx \
--without-djvu \
--without-fontconfig \
--without-freetype \
--without-raqm \
--without-gslib \
--without-gvc \
--without-heic \
--without-jbig \
--with-jpeg \
--without-jxl \
--without-lcms \
--with-openjp2 \
--without-lqr \
--with-lzma \
--without-openexr \
--without-pango \
--with-png \
--without-raw \
--without-rsvg \
--with-tiff \
--without-webp \
--without-wmf \
--without-xml && \
--enable-static=no \
--with-quantum-depth=8 \
--with-modules=no \
--with-frozenpaths=no \
--with-magick-plus-plus=yes \
--with-perl=no \
--with-bzlib=no \
--with-dps=no \
--with-fpx=no \
--with-jbig=no \
--with-webp=no \
--with-jp2=yes \
--with-jpeg=yes \
--with-lcms2=no \
--with-lzma=no \
--with-png=yes \
--with-tiff=yes \
--with-trio=no \
--with-ttf=no \
--with-tcmalloc=no \
--with-wmf=no \
--with-xml=no \
--with-zlib=yes \
--with-zstd=no \
--with-x=no && \
make && \
make install && \
make distclean
@@ -158,8 +151,9 @@ RUN \
RUN \
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include -L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \
make && \
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
make install && \
mkdir -p ${PREFIX}/etc/ && \
cp conf/lms.conf ${PREFIX}/etc
@@ -178,7 +172,8 @@ RUN \
done && \
cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \
cp -r ${PREFIX}/share /tmp/fakeroot/share && \
LD_LIBRARY_PATH=/tmp/fakeroot/lib /tmp/fakeroot/bin/ffmpeg -buildconf
rm -rf /tmp/fakeroot/share/doc && \
rm -rf /tmp/fakeroot/share/man
## Release Stage
FROM alpine:3.10 AS release
@@ -188,6 +183,7 @@ ARG RUNTIME_PACKAGES=" \
openssl \
libjpeg-turbo \
openjpeg \
jasper-libs \
libpng \
tiff \
libogg \
+1
View File
@@ -19,6 +19,7 @@ target_link_libraries(lmsav PUBLIC
lmsutils
avformat
avutil
std::filesystem
wt
)
+7 -3
View File
@@ -10,17 +10,21 @@ target_include_directories(lmscover INTERFACE
target_include_directories(lmscover PRIVATE
include
${IMAGEMAGICKXX_INCLUDE_DIRS}
${GRAPHICSMAGICKXX_INCLUDE_DIRS}
)
target_compile_options(lmscover PRIVATE
${IMAGEMAGICKXX_CFLAGS_OTHER}
${GRAPHICSMAGICKXX_CFLAGS_OTHER}
)
target_link_libraries(lmscover PRIVATE
lmsav
${GRAPHICSMAGICKXX_LIBRARIES}
)
target_link_libraries(lmscover PUBLIC
lmsdatabase
${IMAGEMAGICKXX_LIBRARIES}
std::filesystem
)
install(TARGETS lmscover DESTINATION lib)
+6 -8
View File
@@ -49,15 +49,13 @@ Grabber::Grabber(const std::filesystem::path& execPath)
init(execPath);
}
Grabber::~Grabber()
{
deinit();
}
void
Grabber::setDefaultCover(const std::filesystem::path& p)
{
if (!_defaultCover.load(p))
std::unique_lock lock {_mutex};
_defaultCover = std::make_unique<Image>();
if (!_defaultCover->load(p))
throw LmsException("Cannot read default cover file '" + p.string() + "'");
}
@@ -65,12 +63,12 @@ Image
Grabber::getDefaultCover(std::size_t size)
{
LMS_LOG(COVER, DEBUG) << "Getting a default cover using size = " << size;
std::unique_lock<std::mutex> lock(_mutex);
std::unique_lock lock {_mutex};
auto it = _defaultCovers.find(size);
if (it == _defaultCovers.end())
{
Image cover = _defaultCover;
Image cover = *_defaultCover;
LMS_LOG(COVER, DEBUG) << "default cover size = " << cover.getSize().width << " x " << cover.getSize().height;
+1 -3
View File
@@ -41,7 +41,6 @@ namespace CoverArt
{
public:
Grabber(const std::filesystem::path& execPath);
~Grabber();
Grabber(const Grabber&) = delete;
Grabber& operator=(const Grabber&) = delete;
@@ -63,9 +62,8 @@ namespace CoverArt
std::optional<Image> getFromDirectory(const std::filesystem::path& path) const;
Image getDefaultCover(std::size_t size);
Image _defaultCover;
std::mutex _mutex;
std::unique_ptr<Image> _defaultCover;
std::map<std::size_t /* size */, Image> _defaultCovers;
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
-6
View File
@@ -29,12 +29,6 @@ init(const std::filesystem::path& path)
Magick::InitializeMagick(path.string().c_str());
}
void
deinit()
{
MagickCore::MagickCoreTerminus();
}
static
std::string
formatToMagick(Format format)
-1
View File
@@ -30,7 +30,6 @@ namespace CoverArt
{
void init(const std::filesystem::path& path);
void deinit();
class Image
{
+1
View File
@@ -30,6 +30,7 @@ target_link_libraries(lmsdatabase PRIVATE
target_link_libraries(lmsdatabase PUBLIC
lmsutils
std::filesystem
wtdbo
)
+8 -2
View File
@@ -40,7 +40,7 @@
namespace Database {
#define LMS_DATABASE_VERSION 13
#define LMS_DATABASE_VERSION 14
using Version = std::size_t;
@@ -153,7 +153,13 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" (
}
else if (version == 12)
{
// Artist and release that have a baddly parsed name but a MBID had no chance to updat the name
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else if (version == 13)
{
// Always store UUID in lower case + better WMA parsing
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
+1
View File
@@ -19,6 +19,7 @@ target_link_libraries(lmsmetadata PRIVATE
target_link_libraries(lmsmetadata PUBLIC
lmsutils
std::filesystem
)
install(TARGETS lmsmetadata DESTINATION lib)
+144 -118
View File
@@ -154,12 +154,118 @@ getAlbum(const TagLib::PropertyMap& properties)
return Album {std::move(albumName.front()), albumMBID.front()};
}
void
TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug)
{
// TODO validate MBID format
if (debug)
{
std::vector<std::string> strs;
std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); });
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(strs, "*SEP*") << std::endl;
}
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
return;
std::string value {StringUtils::stringTrim(values.front().to8Bit(true))};
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
{
track.musicBrainzTrackID = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID")
track.musicBrainzRecordID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
else if (tag == "TRACKTOTAL")
{
auto totalTrack = StringUtils::readAs<std::size_t>(value);
if (totalTrack)
track.totalTrack = totalTrack;
}
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {splitAndTrimString(value, "/")};
if (!strings.empty())
{
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
// Lower priority than TRACKTOTAL
if (strings.size() > 1 && !track.totalTrack)
track.totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DISCTOTAL")
{
auto totalDisc = StringUtils::readAs<std::size_t>(value);
if (totalDisc)
track.totalDisc = totalDisc;
}
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {StringUtils::splitString(value, "/")};
if (!strings.empty())
{
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
// Lower priority than DISCTOTAL
if (strings.size() > 1 && !track.totalDisc)
track.totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DATE")
track.year = StringUtils::readAs<int>(value);
else if (tag == "ORIGINALDATE" && !track.originalYear)
{
// Lower priority than ORIGINALYEAR
track.originalYear = StringUtils::readAs<int>(value);
}
else if (tag == "ORIGINALYEAR")
{
// Higher priority than ORIGINALDATE
auto originalYear = StringUtils::readAs<int>(value);
if (originalYear)
track.originalYear = originalYear;
}
else if (tag == "METADATA_BLOCK_PICTURE")
track.hasCover = true;
else if (tag == "COPYRIGHT")
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
for (const auto& valueList : values)
{
auto values = splitAndTrimString(valueList.to8Bit(true), "/,;");
for (const auto& value : values)
clusterNames.insert(value);
}
if (!clusterNames.empty())
track.clusters[tag] = clusterNames;
}
}
std::optional<Track>
TagLibParser::parse(const std::filesystem::path& p, bool debug)
{
TagLib::FileRef f {p.string().c_str(),
true, // read audio properties
TagLib::AudioProperties::Fast};
true, // read audio properties
TagLib::AudioProperties::Fast};
if (f.isNull())
{
@@ -184,14 +290,40 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.audioStreams = {std::move(audioStream)};
}
TagLib::PropertyMap properties {f.file()->properties()};
// Not that good embedded pictures handling
// WMA
if (TagLib::ASF::File* asfFile {dynamic_cast<TagLib::ASF::File*>(f.file())})
{
const TagLib::ASF::Tag* tag {asfFile->tag()};
if (tag && tag->attributeListMap().contains("WM/Picture"))
track.hasCover = true;
if (tag)
{
if (tag->attributeListMap().contains("WM/Picture"))
track.hasCover = true;
for (const auto& [name, attributeList] : tag->attributeListMap())
{
if (name.to8Bit().find("WM/") == 0 || properties.contains(name))
continue;
TagLib::StringList stringAttributeList;
for (const auto& attribute : attributeList)
{
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
stringAttributeList.append(attribute.toString());
}
if (!stringAttributeList.isEmpty())
{
if (debug)
std::cout << "Property: '" << name << "'" << std::endl;
properties.insert(name, stringAttributeList);
}
}
}
}
// MP3
else if (TagLib::MPEG::File* mp3File {dynamic_cast<TagLib::MPEG::File*>(f.file())})
@@ -209,124 +341,18 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true;
}
if (f.tag())
for(const auto& property : properties)
{
MetaData::Clusters clusters;
const TagLib::PropertyMap& properties {f.file()->properties()};
for(const auto& property : properties)
{
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
// TODO validate MBID format
if (debug)
{
std::vector<std::string> strs;
std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); });
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(strs, "*SEP*") << std::endl;
}
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
continue;
std::string value {StringUtils::stringTrim(values.front().to8Bit(true))};
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
{
track.musicBrainzTrackID = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID")
track.musicBrainzRecordID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
else if (tag == "TRACKTOTAL")
{
auto totalTrack = StringUtils::readAs<std::size_t>(value);
if (totalTrack)
track.totalTrack = totalTrack;
}
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {splitAndTrimString(value, "/")};
if (!strings.empty())
{
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
// Lower priority than TRACKTOTAL
if (strings.size() > 1 && !track.totalTrack)
track.totalTrack = StringUtils::readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DISCTOTAL")
{
auto totalDisc = StringUtils::readAs<std::size_t>(value);
if (totalDisc)
track.totalDisc = totalDisc;
}
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {StringUtils::splitString(value, "/")};
if (!strings.empty())
{
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
// Lower priority than DISCTOTAL
if (strings.size() > 1 && !track.totalDisc)
track.totalDisc = StringUtils::readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DATE")
track.year = StringUtils::readAs<int>(value);
else if (tag == "ORIGINALDATE" && !track.originalYear)
{
// Lower priority than ORIGINALYEAR
track.originalYear = StringUtils::readAs<int>(value);
}
else if (tag == "ORIGINALYEAR")
{
// Higher priority than ORIGINALDATE
auto originalYear = StringUtils::readAs<int>(value);
if (originalYear)
track.originalYear = originalYear;
}
else if (tag == "METADATA_BLOCK_PICTURE")
track.hasCover = true;
else if (tag == "COPYRIGHT")
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
for (const auto& valueList : values)
{
auto values = splitAndTrimString(valueList.to8Bit(true), "/,;");
for (const auto& value : values)
clusterNames.insert(value);
}
if (!clusterNames.empty())
track.clusters[tag] = clusterNames;
}
}
track.artists = getArtists(properties);
track.albumArtists = getAlbumArtists(properties);
track.album = getAlbum(properties);
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
processTag(track, tag, values, debug);
}
track.artists = getArtists(properties);
track.albumArtists = getAlbumArtists(properties);
track.album = getAlbum(properties);
return track;
}
@@ -21,14 +21,21 @@
#include "metadata/IParser.hpp"
namespace TagLib
{
class StringList;
}
namespace MetaData
{
// Parse that makes use of AvFormat
class TagLibParser : public IParser
{
public:
private:
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
void processTag(Track& track, const std::string& tag, const TagLib::StringList& values, bool debug);
};
} // namespace MetaData
+1
View File
@@ -18,6 +18,7 @@ target_include_directories(lmsrecommendation PRIVATE
target_link_libraries(lmsrecommendation PRIVATE
lmsdatabase
lmssom
std::filesystem
wt
)
@@ -36,7 +36,7 @@ namespace Recommendation
private:
std::string_view getName() const { return "Clusters"; }
std::string_view getName() const override { return "Clusters"; }
bool init(Database::Session&, bool) override {return true;}
void requestCancelInit() override {}
@@ -55,15 +55,15 @@ class FeaturesClassifier : public IClassifier
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
private:
std::string_view getName() const { return "Features"; }
std::string_view getName() const override { return "Features"; }
bool init(Database::Session& session, bool databaseChanged) override;
void requestCancelInit() override;
std::vector<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
std::vector<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const override;
std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const;
std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const;
std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override;
bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
+1
View File
@@ -20,6 +20,7 @@ target_link_libraries(lmsscanner PRIVATE
)
target_link_libraries(lmsscanner PUBLIC
std::filesystem
wt
)
+1
View File
@@ -21,6 +21,7 @@ target_link_libraries(lmssubsonic PRIVATE
lmscover
lmsrecommendation
lmsutils
std::filesystem
)
target_link_libraries(lmssubsonic PUBLIC
+2 -2
View File
@@ -23,14 +23,14 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
StringUtils::readAs(const std::string& str)
readAs(const std::string& str)
{
return API::Subsonic::IdFromString(str);
}
template<>
std::optional<bool>
StringUtils::readAs(const std::string& str)
readAs(const std::string& str)
{
if (str == "true")
return true;
+2 -2
View File
@@ -90,10 +90,10 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
StringUtils::readAs(const std::string& str);
readAs(const std::string& str);
template<>
std::optional<bool>
StringUtils::readAs(const std::string& str);
readAs(const std::string& str);
}
+2 -1
View File
@@ -19,6 +19,7 @@
#include "subsonic/SubsonicResource.hpp"
#include <atomic>
#include <unordered_map>
#include <Wt/WLocalDateTime.h>
@@ -65,7 +66,7 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::ClientVersion>
StringUtils::readAs(const std::string& str)
readAs(const std::string& str)
{
// Expects "X.Y.Z"
const auto numbers {StringUtils::splitString(str, ".")};
@@ -36,7 +36,7 @@ class SubsonicResource final : public Wt::WResource
public:
SubsonicResource(Database::Db& db);
static std::string getPath() { return "/rest/"; }
static std::string getPath() { return "rest/"; }
private:
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
+1 -1
View File
@@ -26,7 +26,7 @@ target_link_libraries(lmsutils PRIVATE
target_link_libraries(lmsutils PUBLIC
boost_system
stdc++fs
std::filesystem
wt
)
+14 -3
View File
@@ -20,6 +20,7 @@
#include "utils/String.hpp"
#include <iomanip>
#include <unordered_map>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/join.hpp>
@@ -98,15 +99,25 @@ stringTrimEnd(const std::string& str, const std::string& whitespace)
}
std::string
stringToLower(const std::string& str)
stringToLower(std::string_view str)
{
return boost::algorithm::to_lower_copy(str);
std::string res;
res.reserve(str.size());
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::tolower(c);});
return res;
}
std::string
stringToUpper(const std::string& str)
{
return boost::to_upper_copy<std::string>(str);
std::string res;
res.reserve(str.size());
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
return res;
}
std::string
+6
View File
@@ -21,6 +21,8 @@
#include <regex>
#include "utils/String.hpp"
namespace StringUtils
{
template <>
@@ -40,6 +42,10 @@ stringIsUUID(std::string_view str)
return std::regex_match(std::cbegin(str), std::cend(str), re);
}
UUID::UUID(std::string_view str)
: _value {StringUtils::stringToLower(str)}
{
}
std::optional<UUID>
UUID::fromString(std::string_view str)
+2 -1
View File
@@ -21,6 +21,7 @@
#include <optional>
#include <string>
#include <string_view>
#include <sstream>
#include <vector>
@@ -40,7 +41,7 @@ std::string
stringTrimEnd(const std::string& str, const std::string& whitespaces = " \t");
std::string
stringToLower(const std::string& str);
stringToLower(std::string_view str);
std::string
stringToUpper(const std::string& str);
+1 -1
View File
@@ -34,7 +34,7 @@ class UUID
std::string_view getAsString() const { return _value; }
private:
UUID(std::string_view value) : _value {value} {}
UUID(std::string_view value);
std::string _value;
};
+3
View File
@@ -48,6 +48,7 @@ std::vector<std::string> generateWtConfig(std::string execPath)
args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + ServiceProvider<IConfig>::get()->getString("docroot"));
args.push_back("--approot=" + ServiceProvider<IConfig>::get()->getString("approot"));
args.push_back("--deploy-path=" + ServiceProvider<IConfig>::get()->getString("deploy-path", "/"));
args.push_back("--resources-dir=" + ServiceProvider<IConfig>::get()->getString("wt-resources"));
if (ServiceProvider<IConfig>::get()->getBool("tls-enable", false))
@@ -197,6 +198,8 @@ int main(int argc, char* argv[])
LMS_LOG(MAIN, INFO) << "Stopping recommendation engine...";
recommendationEngine.stop();
ServiceProvider<CoverArt::IGrabber>::clear();
LMS_LOG(MAIN, INFO) << "Clean stop!";
res = EXIT_SUCCESS;
}
@@ -88,7 +88,7 @@ dumpRecommendation(Database::Session session, Recommendation::IEngine& engine)
};
std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl;
for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, {releaseId}, 3))
for (Database::IdType similarReleaseId : engine.getSimilarReleases(session, releaseId, 3))
std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl;
}
@@ -110,7 +110,7 @@ dumpRecommendation(Database::Session session, Recommendation::IEngine& engine)
};
std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl;
for (Database::IdType similarArtistId : engine.getSimilarArtists(session, {artistId}, 3))
for (Database::IdType similarArtistId : engine.getSimilarArtists(session, artistId, 3))
std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl;
}