Merge branch 'develop' for release 3.7.0
This commit is contained in:
+2
-4
@@ -16,15 +16,13 @@ matrix:
|
||||
- gcc
|
||||
before_install:
|
||||
- eval "${MATRIX_EVAL}"
|
||||
- sudo apt-get install build-essential autoconf automake 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 libmagick++-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
|
||||
- cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && sudo make install
|
||||
- popd
|
||||
script: autoreconf -vfi && CXXFLAGS="-Wall -Wextra -Werror" ./configure --enable-tools && make distcheck
|
||||
script: cmake -DCMAKE_BUILD_TYPE=Release . && make && make test
|
||||
env:
|
||||
global:
|
||||
- MAKEFLAGS="-j 2"
|
||||
after_failure:
|
||||
- cat config.log
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(lms)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
include(CTest)
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(IMAGEMAGICKXX REQUIRED ImageMagick++)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
install(DIRECTORY approot DESTINATION share/lms)
|
||||
install(DIRECTORY docroot DESTINATION share/lms)
|
||||
install(FILES systemd/default.service DESTINATION share/lms)
|
||||
install(FILES conf/lms.conf DESTINATION share/lms)
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
|
||||
AUTOMAKE_OPTIONS = dist-bzip2 no-dist-gzip
|
||||
|
||||
SUBDIRS = test tools src
|
||||
|
||||
lms_docrootdir=$(pkgdatadir)/docroot
|
||||
lms_approotdir=$(pkgdatadir)/approot
|
||||
|
||||
lms_cssdir=$(lms_docrootdir)/css
|
||||
lms_jsdir=$(lms_docrootdir)/js
|
||||
lms_imagesdir=$(lms_approotdir)/images
|
||||
|
||||
dist_pkgdata_DATA = \
|
||||
systemd/default.service \
|
||||
conf/lms.conf
|
||||
|
||||
dist_lms_css_DATA = \
|
||||
docroot/css/lms.css
|
||||
|
||||
dist_lms_js_DATA = \
|
||||
docroot/js/bootstrap-notify.js \
|
||||
docroot/js/jquery-1.10.2.min.js \
|
||||
docroot/js/mediaplayer.js
|
||||
|
||||
dist_lms_images_DATA = \
|
||||
approot/images/unknown-cover.jpg \
|
||||
approot/images/unknown-artist.jpg
|
||||
|
||||
dist_lms_approot_DATA = \
|
||||
approot/admin-database.xml \
|
||||
approot/admin-user.xml \
|
||||
approot/admin-users.xml \
|
||||
approot/admin-initwizard.xml \
|
||||
approot/artist.xml \
|
||||
approot/artistinfo.xml \
|
||||
approot/artistlink.xml \
|
||||
approot/artists.xml \
|
||||
approot/artistsinfo.xml \
|
||||
approot/error.xml \
|
||||
approot/explore.xml \
|
||||
approot/login.xml \
|
||||
approot/mediaplayer.xml \
|
||||
approot/messages.xml \
|
||||
approot/messages_fr.xml \
|
||||
approot/playhistory.xml \
|
||||
approot/playqueue.xml \
|
||||
approot/release.xml \
|
||||
approot/releaseinfo.xml \
|
||||
approot/releaselink.xml \
|
||||
approot/releases.xml \
|
||||
approot/releasesinfo.xml \
|
||||
approot/settings.xml \
|
||||
approot/templates.xml \
|
||||
approot/tracks.xml \
|
||||
approot/tracksinfo.xml
|
||||
|
||||
@@ -17,28 +17,33 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
|
||||
* Audio transcode for maximum interoperability and low bandwith requirements
|
||||
* Persistent play queue across sessions
|
||||
* Subsonic API
|
||||
* Album artist
|
||||
* Compilation support
|
||||
* Multi-value tags: artists, genres, ...
|
||||
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
||||
* MusicBrainzID support to handle duplicated artist and release names
|
||||
* Playlists, (only using Subsonic API for now)
|
||||
* Starred Album/Artist/Tracks (only using Subsonic API for now)
|
||||
* _Systemd_ integration
|
||||
* Subsonic-only features:
|
||||
* Playlists
|
||||
* Starred Album/Artist/Tracks
|
||||
* Bookmarks
|
||||
|
||||
## Recommendation engine
|
||||
## Music discovery
|
||||
|
||||
_LMS_ provides several ways to help you find the music you like:
|
||||
* Tag-based filters (ex: _Rock_, _Metal_ and _Aggressive_, _Electronic_ and _Relaxed_, ...)
|
||||
* Recommendations for similar artists and albums
|
||||
* Radio mode
|
||||
* Radio mode, based on what is in the current playqueue
|
||||
* Searches in album, artist and track names
|
||||
* Most played/Recently added music
|
||||
|
||||
The recommendation engine makes use of [Self-Organizing Maps](https://en.wikipedia.org/wiki/Self-organizing_map).</br>
|
||||
__Notes__:
|
||||
* constructing the map requires significant computation time on large collections (ex: half an hour for 40k tracks)
|
||||
* audio data is pulled from [AcousticBrainz](https://acousticbrainz.org/). Therefore your music files must contain the [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) for the recommendation engine to work properly (otherwise, only tag-based recommendations are provided)
|
||||
* to use the _self-organizing map_ based engine, you have to enable it first in the settings panel.
|
||||
The recommendation engine uses two different sources:
|
||||
1. Tags that are present in the audio files
|
||||
2. Acoustic similarities of the audio files, using a trained [Self-Organizing Map](https://en.wikipedia.org/wiki/Self-organizing_map)
|
||||
|
||||
__Notes on the self-organizing map__:
|
||||
* training the map requires significant computation time on large collections (ex: half an hour for 40k tracks)
|
||||
* audio acoustic data is pulled from [AcousticBrainz](https://acousticbrainz.org/). Therefore your audio files _must_ contain the [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier).
|
||||
* to enable the audio similarity source, you have to enable it first in the settings panel.
|
||||
|
||||
## Subsonic API
|
||||
The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_.
|
||||
@@ -80,7 +85,7 @@ __Notes__:
|
||||
* a C++17 compiler is needed
|
||||
* ffmpeg version 4 minimum is required
|
||||
```sh
|
||||
apt-get install g++ autoconf automake libboost-filesystem-dev libboost-system-dev libavutil-dev libavformat-dev libmagick++-dev libpstreams-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev
|
||||
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libmagick++-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>
|
||||
@@ -92,16 +97,10 @@ Get the latest stable release and build it:
|
||||
```sh
|
||||
git clone https://github.com/epoupon/lms.git lms
|
||||
cd lms
|
||||
autoreconf -vfi
|
||||
mkdir build
|
||||
cd build
|
||||
../configure --prefix=/usr
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
```
|
||||
configure will report any missing library.
|
||||
|
||||
__Note__: in order to customize the installation directories, you can use the following options of the `configure` script:
|
||||
* _--prefix_ (defaults to `/usr/local`).
|
||||
* _--bindir_ (defaults to `$PREFIX/bin`).
|
||||
__Note__: in order to customize the installation directory, you can use the _-DCMAKE_INSTALL_PREFIX_ option (defaults to `/usr/local`).
|
||||
|
||||
```sh
|
||||
make
|
||||
|
||||
@@ -47,14 +47,14 @@
|
||||
<div class="form-horizontal">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:similarity-engine-type}">
|
||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
||||
<label class="control-label col-sm-2" for="${id:recommendation-engine-type}">
|
||||
${tr:Lms.Admin.Database.recommendation-engine-type}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${similarity-engine-type}
|
||||
${recommendation-engine-type}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${similarity-engine-type-info}
|
||||
${recommendation-engine-type-info}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||
<message id="Lms.Admin.Database.never">Never</message>
|
||||
<message id="Lms.Admin.Database.path">Media root directory</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type">Recommendation engine</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Tags based</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.features">Audio analysis based</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">Recommendation engine</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Tags based</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.features">Audio analysis based</message>
|
||||
<message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message>
|
||||
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">Scan options</message>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||
<message id="Lms.Admin.Database.never">Jamais</message>
|
||||
<message id="Lms.Admin.Database.path">Dossier racine des fichiers de musique</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type">Moteur de recommandation</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basé sur les tags</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.features">Basé sur l'analyse audio</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type">Moteur de recommandation</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.clusters">Basé sur les tags</message>
|
||||
<message id="Lms.Admin.Database.recommendation-engine-type.features">Basé sur l'analyse audio</message>
|
||||
<message id="Lms.Admin.Database.scan-complete">Scan terminé : {1} fichiers, {2} ajouts, {3} mises à jour, {4} suppressions, {5} duplicatas, {6} erreurs</message>
|
||||
<message id="Lms.Admin.Database.scan-launched">Scan lancé !</message>
|
||||
<message id="Lms.Admin.Database.scan-options">Options </message>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
AC_PREREQ(2.59)
|
||||
AC_INIT(lms, 3.6.3, test@test)
|
||||
AM_INIT_AUTOMAKE
|
||||
AC_CONFIG_HEADER(src/config/config.h)
|
||||
|
||||
AC_LANG_CPLUSPLUS
|
||||
|
||||
AC_PROG_CXX
|
||||
|
||||
PKG_CHECK_MODULES(IMAGEMAGICKXX, "ImageMagick++", [ HAVE_IMAGEMAGICKXX=yes ], [ ])
|
||||
if test -n "$HAVE_IMAGEMAGICKXX"; then
|
||||
MAGICKXX_CFLAGS="$IMAGEMAGICKXX_CFLAGS"
|
||||
MAGICKXX_LIBS="$IMAGEMAGICKXX_LIBS"
|
||||
fi
|
||||
AC_SUBST(MAGICKXX_CFLAGS)
|
||||
AC_SUBST(MAGICKXX_LIBS)
|
||||
|
||||
AC_CHECK_HEADERS([Wt/WApplication.h pstreams/pstream.h boost/asio.hpp],
|
||||
[],
|
||||
[AC_MSG_ERROR([Header not found or unusable !])])
|
||||
|
||||
AC_CHECK_LIB([stdc++fs],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([lib filesystem not found!])])
|
||||
|
||||
AC_CHECK_LIB([pthread],
|
||||
[pthread_rwlock_unlock],
|
||||
,
|
||||
[AC_MSG_ERROR([libpthread not found!])])
|
||||
|
||||
AC_CHECK_LIB([boost_system],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libboost_system not found!])])
|
||||
|
||||
AC_CHECK_LIB([wt],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libwt not found!])])
|
||||
|
||||
AC_CHECK_LIB([wtdbo],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libwtdbo not found!])])
|
||||
|
||||
AC_CHECK_LIB([wtdbosqlite3],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libwtdbosqlite3 not found!])])
|
||||
|
||||
AC_CHECK_LIB([wthttp],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libwthttp not found!])])
|
||||
|
||||
|
||||
AC_CHECK_LIB([avutil],
|
||||
[av_free],
|
||||
,
|
||||
[AC_MSG_ERROR([libavutil not found!])])
|
||||
|
||||
AC_CHECK_LIB([avformat],
|
||||
[av_read_frame],
|
||||
,
|
||||
[AC_MSG_ERROR([libavformat not found!])])
|
||||
|
||||
AC_CHECK_LIB( [tag],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libtag not found!])])
|
||||
|
||||
AC_CHECK_LIB( [config++],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libconfig++ not found!])])
|
||||
|
||||
AC_CONFIG_FILES([Makefile
|
||||
src/Makefile
|
||||
test/Makefile
|
||||
tools/Makefile
|
||||
tools/similarity/Makefile
|
||||
tools/similarity-parameters/Makefile
|
||||
tools/metadata/Makefile])
|
||||
|
||||
AC_ARG_ENABLE([tools],
|
||||
[AC_HELP_STRING([--enable-tools], [Build the tools])],
|
||||
[:],
|
||||
[enable_tools=no])
|
||||
|
||||
AM_CONDITIONAL([BUILD_TOOLS], [test "$enable_tools" = "yes"])
|
||||
|
||||
AC_OUTPUT
|
||||
|
||||
+5
-7
@@ -7,7 +7,7 @@ ARG FFMPEG_VERSION=4.1.4
|
||||
ARG WT_VERSION=4.2.0
|
||||
ARG IMAGEMAGICK6_VERSION=6.9.10-71
|
||||
ARG PSTREAMS_VERSION=1.0.1
|
||||
ARG LMS_VERSION=3.5.0
|
||||
ARG LMS_VERSION=v3.6.3
|
||||
|
||||
ARG PREFIX="/tmp/install"
|
||||
|
||||
@@ -153,23 +153,21 @@ RUN \
|
||||
# LMS
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://github.com/epoupon/lms/archive/v${LMS_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f v${LMS_VERSION}.tar.gz
|
||||
curl -sL https://github.com/epoupon/lms/archive/${LMS_VERSION}.tar.gz -o ${LMS_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f ${LMS_VERSION}.tar.gz
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
autoreconf -vfi && \
|
||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-O2 -I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" ./configure --prefix=${PREFIX} && \
|
||||
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} && \
|
||||
make && \
|
||||
make install && \
|
||||
make distclean && \
|
||||
mkdir -p ${PREFIX}/etc/ && \
|
||||
cp conf/lms.conf ${PREFIX}/etc
|
||||
|
||||
# Now copy all the stuff installed in a new folder (/tmp/fakeroot/)
|
||||
RUN \
|
||||
mkdir -p /tmp/fakeroot/bin && \
|
||||
for bin in ${PREFIX}/bin/ffmpeg ${PREFIX}/bin/lms; \
|
||||
for bin in ${PREFIX}/bin/ffmpeg ${PREFIX}/bin/lms*; \
|
||||
do \
|
||||
strip --strip-all $bin && \
|
||||
cp $bin /tmp/fakeroot/bin/; \
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
add_compile_options(-Wall -Wextra -pedantic)
|
||||
|
||||
add_subdirectory(libs)
|
||||
add_subdirectory(lms)
|
||||
add_subdirectory(tools)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
bin_PROGRAMS = lms
|
||||
|
||||
lms_SOURCES = \
|
||||
$(srcdir)/api/subsonic/SubsonicId.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicId.hpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResource.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResource.hpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResponse.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResponse.hpp \
|
||||
$(srcdir)/auth/AuthTokenService.cpp \
|
||||
$(srcdir)/auth/AuthTokenService.hpp \
|
||||
$(srcdir)/auth/PasswordService.cpp \
|
||||
$(srcdir)/auth/PasswordService.hpp \
|
||||
$(srcdir)/auth/LoginThrottler.cpp \
|
||||
$(srcdir)/auth/LoginThrottler.hpp \
|
||||
$(srcdir)/av/AvInfo.cpp \
|
||||
$(srcdir)/av/AvInfo.hpp \
|
||||
$(srcdir)/av/AvTranscoder.cpp \
|
||||
$(srcdir)/av/AvTranscoder.hpp \
|
||||
$(srcdir)/av/AvTypes.cpp \
|
||||
$(srcdir)/av/AvTypes.hpp \
|
||||
$(srcdir)/cover/CoverArtGrabber.cpp \
|
||||
$(srcdir)/cover/CoverArtGrabber.hpp \
|
||||
$(srcdir)/database/Artist.cpp \
|
||||
$(srcdir)/database/Artist.hpp \
|
||||
$(srcdir)/database/Cluster.cpp \
|
||||
$(srcdir)/database/Cluster.hpp \
|
||||
$(srcdir)/database/Db.cpp \
|
||||
$(srcdir)/database/Db.hpp \
|
||||
$(srcdir)/database/TrackArtistLink.cpp \
|
||||
$(srcdir)/database/TrackArtistLink.hpp \
|
||||
$(srcdir)/database/TrackFeatures.cpp \
|
||||
$(srcdir)/database/TrackFeatures.hpp \
|
||||
$(srcdir)/database/TrackList.cpp \
|
||||
$(srcdir)/database/TrackList.hpp \
|
||||
$(srcdir)/database/Types.hpp \
|
||||
$(srcdir)/database/Release.cpp \
|
||||
$(srcdir)/database/Release.hpp \
|
||||
$(srcdir)/database/ScanSettings.cpp \
|
||||
$(srcdir)/database/ScanSettings.hpp \
|
||||
$(srcdir)/database/Session.cpp \
|
||||
$(srcdir)/database/Session.hpp \
|
||||
$(srcdir)/database/SessionPool.cpp \
|
||||
$(srcdir)/database/SessionPool.hpp \
|
||||
$(srcdir)/database/SqlQuery.cpp \
|
||||
$(srcdir)/database/SqlQuery.hpp \
|
||||
$(srcdir)/database/Track.cpp \
|
||||
$(srcdir)/database/Track.hpp \
|
||||
$(srcdir)/database/TrackBookmark.cpp \
|
||||
$(srcdir)/database/TrackBookmark.hpp \
|
||||
$(srcdir)/database/User.cpp \
|
||||
$(srcdir)/database/User.hpp \
|
||||
$(srcdir)/image/Image.cpp \
|
||||
$(srcdir)/image/Image.hpp \
|
||||
$(srcdir)/main/main.cpp \
|
||||
$(srcdir)/metadata/AvFormat.cpp \
|
||||
$(srcdir)/metadata/AvFormat.hpp \
|
||||
$(srcdir)/metadata/MetaData.hpp \
|
||||
$(srcdir)/metadata/TagLibParser.cpp \
|
||||
$(srcdir)/metadata/TagLibParser.hpp \
|
||||
$(srcdir)/scanner/MediaScanner.cpp \
|
||||
$(srcdir)/scanner/MediaScanner.hpp \
|
||||
$(srcdir)/scanner/MediaScannerStats.cpp \
|
||||
$(srcdir)/scanner/MediaScannerStats.hpp \
|
||||
$(srcdir)/scanner/MediaScannerAddon.hpp \
|
||||
$(srcdir)/similarity/SimilaritySearcher.cpp \
|
||||
$(srcdir)/similarity/SimilaritySearcher.hpp \
|
||||
$(srcdir)/similarity/cluster/SimilarityClusterSearcher.cpp \
|
||||
$(srcdir)/similarity/cluster/SimilarityClusterSearcher.hpp \
|
||||
$(srcdir)/similarity/features/AcousticBrainzUtils.cpp \
|
||||
$(srcdir)/similarity/features/AcousticBrainzUtils.hpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesCache.cpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesCache.hpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesDefs.cpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesDefs.hpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesScannerAddon.cpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesScannerAddon.hpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesSearcher.cpp \
|
||||
$(srcdir)/similarity/features/SimilarityFeaturesSearcher.hpp \
|
||||
$(srcdir)/similarity/features/som/DataNormalizer.cpp \
|
||||
$(srcdir)/similarity/features/som/DataNormalizer.hpp \
|
||||
$(srcdir)/similarity/features/som/InputVector.hpp \
|
||||
$(srcdir)/similarity/features/som/Matrix.hpp \
|
||||
$(srcdir)/similarity/features/som/Network.cpp \
|
||||
$(srcdir)/similarity/features/som/Network.hpp \
|
||||
$(srcdir)/ui/Auth.cpp \
|
||||
$(srcdir)/ui/Auth.hpp \
|
||||
$(srcdir)/ui/LmsApplication.cpp \
|
||||
$(srcdir)/ui/LmsApplication.hpp \
|
||||
$(srcdir)/ui/LmsApplicationException.hpp \
|
||||
$(srcdir)/ui/LmsApplicationGroup.cpp \
|
||||
$(srcdir)/ui/LmsApplicationGroup.hpp \
|
||||
$(srcdir)/ui/MediaPlayer.cpp \
|
||||
$(srcdir)/ui/MediaPlayer.hpp \
|
||||
$(srcdir)/ui/PlayQueueView.cpp \
|
||||
$(srcdir)/ui/PlayQueueView.hpp \
|
||||
$(srcdir)/ui/PlayHistoryView.cpp \
|
||||
$(srcdir)/ui/PlayHistoryView.hpp \
|
||||
$(srcdir)/ui/SettingsView.cpp \
|
||||
$(srcdir)/ui/SettingsView.hpp \
|
||||
$(srcdir)/ui/TrackStringUtils.cpp \
|
||||
$(srcdir)/ui/TrackStringUtils.hpp \
|
||||
$(srcdir)/ui/admin/DatabaseSettingsView.cpp \
|
||||
$(srcdir)/ui/admin/DatabaseSettingsView.hpp \
|
||||
$(srcdir)/ui/admin/DatabaseStatus.cpp \
|
||||
$(srcdir)/ui/admin/DatabaseStatus.hpp \
|
||||
$(srcdir)/ui/admin/InitWizardView.cpp \
|
||||
$(srcdir)/ui/admin/InitWizardView.hpp \
|
||||
$(srcdir)/ui/admin/UserView.cpp \
|
||||
$(srcdir)/ui/admin/UserView.hpp \
|
||||
$(srcdir)/ui/admin/UsersView.cpp \
|
||||
$(srcdir)/ui/admin/UsersView.hpp \
|
||||
$(srcdir)/ui/common/Validators.cpp \
|
||||
$(srcdir)/ui/common/Validators.hpp \
|
||||
$(srcdir)/ui/common/ValueStringModel.hpp \
|
||||
$(srcdir)/ui/explore/ArtistInfoView.cpp \
|
||||
$(srcdir)/ui/explore/ArtistInfoView.hpp \
|
||||
$(srcdir)/ui/explore/ArtistLink.cpp \
|
||||
$(srcdir)/ui/explore/ArtistLink.hpp \
|
||||
$(srcdir)/ui/explore/ArtistsInfoView.cpp \
|
||||
$(srcdir)/ui/explore/ArtistsInfoView.hpp \
|
||||
$(srcdir)/ui/explore/ArtistView.cpp \
|
||||
$(srcdir)/ui/explore/ArtistView.hpp \
|
||||
$(srcdir)/ui/explore/ArtistsView.cpp \
|
||||
$(srcdir)/ui/explore/ArtistsView.hpp \
|
||||
$(srcdir)/ui/explore/Explore.cpp \
|
||||
$(srcdir)/ui/explore/Explore.hpp \
|
||||
$(srcdir)/ui/explore/Filters.cpp \
|
||||
$(srcdir)/ui/explore/Filters.hpp \
|
||||
$(srcdir)/ui/explore/ReleaseInfoView.cpp \
|
||||
$(srcdir)/ui/explore/ReleaseInfoView.hpp \
|
||||
$(srcdir)/ui/explore/ReleaseLink.cpp \
|
||||
$(srcdir)/ui/explore/ReleaseLink.hpp \
|
||||
$(srcdir)/ui/explore/ReleasesInfoView.cpp \
|
||||
$(srcdir)/ui/explore/ReleasesInfoView.hpp \
|
||||
$(srcdir)/ui/explore/ReleasesView.cpp \
|
||||
$(srcdir)/ui/explore/ReleasesView.hpp \
|
||||
$(srcdir)/ui/explore/ReleaseView.cpp \
|
||||
$(srcdir)/ui/explore/ReleaseView.hpp \
|
||||
$(srcdir)/ui/explore/TracksInfoView.cpp \
|
||||
$(srcdir)/ui/explore/TracksInfoView.hpp \
|
||||
$(srcdir)/ui/explore/TracksView.cpp \
|
||||
$(srcdir)/ui/explore/TracksView.hpp \
|
||||
$(srcdir)/ui/resource/ImageResource.cpp \
|
||||
$(srcdir)/ui/resource/ImageResource.hpp \
|
||||
$(srcdir)/ui/resource/AudioResource.cpp \
|
||||
$(srcdir)/ui/resource/AudioResource.hpp \
|
||||
$(srcdir)/utils/Config.cpp \
|
||||
$(srcdir)/utils/Config.hpp \
|
||||
$(srcdir)/utils/Exception.hpp \
|
||||
$(srcdir)/utils/Logger.cpp \
|
||||
$(srcdir)/utils/Logger.hpp \
|
||||
$(srcdir)/utils/NetAddress.cpp \
|
||||
$(srcdir)/utils/NetAddress.hpp \
|
||||
$(srcdir)/utils/Path.cpp \
|
||||
$(srcdir)/utils/Path.hpp \
|
||||
$(srcdir)/utils/Random.cpp \
|
||||
$(srcdir)/utils/Random.hpp \
|
||||
$(srcdir)/utils/Service.hpp \
|
||||
$(srcdir)/utils/StreamLogger.cpp \
|
||||
$(srcdir)/utils/StreamLogger.hpp \
|
||||
$(srcdir)/utils/String.cpp \
|
||||
$(srcdir)/utils/String.hpp \
|
||||
$(srcdir)/utils/Utils.hpp \
|
||||
$(srcdir)/utils/UUID.cpp \
|
||||
$(srcdir)/utils/UUID.hpp \
|
||||
$(srcdir)/utils/WtLogger.cpp \
|
||||
$(srcdir)/utils/WtLogger.hpp
|
||||
|
||||
lms_CXXFLAGS=-std=c++17 -I$(srcdir)/ui $(MAGICKXX_CFLAGS) -D_REENTRANT
|
||||
lms_LDADD=$(MAGICKXX_LIBS)
|
||||
|
||||
@@ -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 <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "image/Image.hpp"
|
||||
|
||||
namespace Database {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace CoverArt {
|
||||
|
||||
class Grabber
|
||||
{
|
||||
public:
|
||||
Grabber();
|
||||
Grabber(const Grabber&) = delete;
|
||||
Grabber& operator=(const Grabber&) = delete;
|
||||
Grabber(Grabber&&) = delete;
|
||||
Grabber& operator=(Grabber&&) = delete;
|
||||
|
||||
void setDefaultCover(const std::filesystem::path& defaultCoverPath);
|
||||
|
||||
std::vector<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Image::Format format, std::size_t size);
|
||||
std::vector<uint8_t> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Image::Format format, std::size_t size);
|
||||
|
||||
private:
|
||||
|
||||
Image::Image getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size);
|
||||
Image::Image getFromRelease(Database::Session& dbSession, Database::IdType releaseId, std::size_t size);
|
||||
|
||||
std::optional<Image::Image> getFromTrack(const std::filesystem::path& path) const;
|
||||
std::vector<std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
|
||||
std::optional<Image::Image> getFromDirectory(const std::filesystem::path& path) const;
|
||||
|
||||
Image::Image getDefaultCover(std::size_t size);
|
||||
|
||||
Image::Image _defaultCover;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::map<std::size_t /* size */, Image::Image> _defaultCovers;
|
||||
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
|
||||
static inline const std::size_t _maxFileSize {10000000};
|
||||
|
||||
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(av)
|
||||
add_subdirectory(cover)
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(metadata)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(scanner)
|
||||
add_subdirectory(som)
|
||||
add_subdirectory(subsonic)
|
||||
add_subdirectory(utils)
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
add_library(lmsauth SHARED
|
||||
impl/AuthTokenService.cpp
|
||||
impl/PasswordService.cpp
|
||||
impl/LoginThrottler.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsauth INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsauth PRIVATE
|
||||
include/
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PRIVATE
|
||||
lmsutils
|
||||
lmsdatabase
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PUBLIC
|
||||
pthread
|
||||
boost_system
|
||||
wt
|
||||
)
|
||||
|
||||
install(TARGETS lmsauth DESTINATION lib)
|
||||
|
||||
@@ -26,11 +26,17 @@
|
||||
#include <Wt/WRandom.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries)
|
||||
{
|
||||
return std::make_unique<AuthTokenService>(maxThrottlerEntries);
|
||||
}
|
||||
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries)
|
||||
@@ -21,14 +21,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -38,7 +33,7 @@ namespace Database
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class AuthTokenService
|
||||
class AuthTokenService : public IAuthTokenService
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -52,30 +47,8 @@ namespace Auth {
|
||||
AuthTokenService(AuthTokenService&&) = delete;
|
||||
AuthTokenService& operator=(AuthTokenService&&) = delete;
|
||||
|
||||
|
||||
// Auth Token services
|
||||
struct AuthTokenProcessResult
|
||||
{
|
||||
enum class State
|
||||
{
|
||||
Found,
|
||||
Throttled,
|
||||
NotFound,
|
||||
};
|
||||
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
Database::IdType userId;
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
State state {State::NotFound};
|
||||
std::optional<AuthTokenInfo> authTokenInfo {};
|
||||
};
|
||||
|
||||
// Removed if found
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue);
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry);
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
|
||||
namespace Auth {
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::size_t maxThrottlerEntries)
|
||||
{
|
||||
return std::make_unique<PasswordService>(maxThrottlerEntries);
|
||||
}
|
||||
|
||||
PasswordService::PasswordService(std::size_t maxThrottlerEntries)
|
||||
: _loginThrottler{maxThrottlerEntries}
|
||||
{
|
||||
@@ -17,17 +17,12 @@
|
||||
* 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 <string>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <shared_mutex>
|
||||
#include "auth/IPasswordService.hpp"
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -37,7 +32,7 @@ namespace Database
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class PasswordService
|
||||
class PasswordService : public IPasswordService
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -53,39 +48,9 @@ namespace Auth {
|
||||
|
||||
|
||||
// Password services
|
||||
enum class PasswordCheckResult
|
||||
{
|
||||
Match,
|
||||
Mismatch,
|
||||
Throttled,
|
||||
};
|
||||
PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password);
|
||||
Database::User::PasswordHash hashPassword(const std::string& password) const;
|
||||
bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const;
|
||||
|
||||
// Auth Token services
|
||||
struct AuthTokenProcessResult
|
||||
{
|
||||
enum class State
|
||||
{
|
||||
Found,
|
||||
Throttled,
|
||||
NotFound,
|
||||
};
|
||||
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
Database::IdType userId;
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
State state;
|
||||
std::optional<AuthTokenInfo> authTokenInfo;
|
||||
};
|
||||
|
||||
// Removed if found
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue);
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry);
|
||||
PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) override;
|
||||
Database::User::PasswordHash hashPassword(const std::string& password) const override;
|
||||
bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <optional>
|
||||
#include <string>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class IAuthTokenService
|
||||
{
|
||||
public:
|
||||
|
||||
// Auth Token services
|
||||
struct AuthTokenProcessResult
|
||||
{
|
||||
enum class State
|
||||
{
|
||||
Found,
|
||||
Throttled,
|
||||
NotFound,
|
||||
};
|
||||
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
Database::IdType userId;
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
State state {State::NotFound};
|
||||
std::optional<AuthTokenInfo> authTokenInfo {};
|
||||
};
|
||||
|
||||
// Removed if found
|
||||
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) = 0;
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntryCount);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <string>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
#include "database/User.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class IPasswordService
|
||||
{
|
||||
public:
|
||||
|
||||
virtual ~IPasswordService() = default;
|
||||
|
||||
// Password services
|
||||
enum class PasswordCheckResult
|
||||
{
|
||||
Match,
|
||||
Mismatch,
|
||||
Throttled,
|
||||
};
|
||||
virtual PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) = 0;
|
||||
virtual Database::User::PasswordHash hashPassword(const std::string& password) const = 0;
|
||||
virtual bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::size_t maxThrottlerEntryCount);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
add_library(lmsav SHARED
|
||||
impl/AvInfo.cpp
|
||||
impl/AvTranscoder.cpp
|
||||
impl/AvTypes.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsav INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsav PRIVATE
|
||||
include/
|
||||
)
|
||||
|
||||
# TODO make these private
|
||||
target_link_libraries(lmsav PUBLIC
|
||||
lmsutils
|
||||
avformat
|
||||
avutil
|
||||
)
|
||||
|
||||
install(TARGETS lmsav DESTINATION lib)
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvInfo.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include "av/AvInfo.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
@@ -92,7 +90,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
|
||||
AVDictionaryEntry *tag = NULL;
|
||||
while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
{
|
||||
res[boost::to_upper_copy<std::string>(tag->key)] = tag->value;
|
||||
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +139,7 @@ MediaFile::getStreamInfo() const
|
||||
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
|
||||
continue;
|
||||
|
||||
res.push_back( {.id = i, .bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate)} );
|
||||
res.push_back( {i, static_cast<std::size_t>(avstream->codecpar->bit_rate)} );
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -17,13 +17,13 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvTranscoder.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "AvInfo.hpp"
|
||||
#include "utils/Config.hpp"
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
@@ -38,7 +38,7 @@ static std::filesystem::path ffmpegPath;
|
||||
void
|
||||
Transcoder::init()
|
||||
{
|
||||
ffmpegPath = ServiceProvider<Config>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
ffmpegPath = ServiceProvider<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(ffmpegPath))
|
||||
throw LmsException {"File '" + ffmpegPath.string() + "' does not exist!"};
|
||||
}
|
||||
@@ -17,9 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvTypes.hpp"
|
||||
|
||||
#include <map>
|
||||
#include "av/AvTypes.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
@@ -34,7 +32,7 @@ const char* encodingToMimetype(Encoding encoding)
|
||||
case Encoding::WEBM_VORBIS: return "audio/webm";
|
||||
}
|
||||
|
||||
throw AvException("Invalid encoding");
|
||||
throw AvException {"Invalid encoding"};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
add_library(lmscover SHARED
|
||||
impl/CoverArtGrabber.cpp
|
||||
impl/Image.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmscover INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmscover PRIVATE
|
||||
include
|
||||
${IMAGEMAGICKXX_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_compile_options(lmscover PRIVATE
|
||||
${IMAGEMAGICKXX_CFLAGS_OTHER}
|
||||
)
|
||||
|
||||
target_link_libraries(lmscover PRIVATE
|
||||
lmsav
|
||||
lmsdatabase
|
||||
${IMAGEMAGICKXX_LIBRARIES}
|
||||
)
|
||||
|
||||
install(TARGETS lmscover DESTINATION lib)
|
||||
|
||||
@@ -39,8 +39,19 @@ isFileSupported(const std::filesystem::path& file, const std::vector<std::filesy
|
||||
|
||||
namespace CoverArt {
|
||||
|
||||
Grabber::Grabber()
|
||||
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath)
|
||||
{
|
||||
return std::make_unique<Grabber>(execPath);
|
||||
}
|
||||
|
||||
Grabber::Grabber(const std::filesystem::path& execPath)
|
||||
{
|
||||
init(execPath);
|
||||
}
|
||||
|
||||
Grabber::~Grabber()
|
||||
{
|
||||
deinit();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -50,7 +61,7 @@ Grabber::setDefaultCover(const std::filesystem::path& p)
|
||||
throw LmsException("Cannot read default cover file '" + p.string() + "'");
|
||||
}
|
||||
|
||||
Image::Image
|
||||
Image
|
||||
Grabber::getDefaultCover(std::size_t size)
|
||||
{
|
||||
LMS_LOG(COVER, DEBUG) << "Getting a default cover using size = " << size;
|
||||
@@ -59,12 +70,12 @@ Grabber::getDefaultCover(std::size_t size)
|
||||
auto it = _defaultCovers.find(size);
|
||||
if (it == _defaultCovers.end())
|
||||
{
|
||||
Image::Image cover = _defaultCover;
|
||||
Image cover = _defaultCover;
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "default cover size = " << cover.getSize().width << " x " << cover.getSize().height;
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "Scaling cover to size = " << size;
|
||||
cover.scale(Image::Geometry{size, size});
|
||||
cover.scale(Geometry{size, size});
|
||||
LMS_LOG(COVER, DEBUG) << "Scaling DONE";
|
||||
auto res = _defaultCovers.insert(std::make_pair(size, cover));
|
||||
assert(res.second);
|
||||
@@ -74,14 +85,14 @@ Grabber::getDefaultCover(std::size_t size)
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static std::optional<Image::Image>
|
||||
static std::optional<Image>
|
||||
getFromAvMediaFile(const Av::MediaFile& input)
|
||||
{
|
||||
std::vector<Image::Image> res;
|
||||
std::vector<Image> res;
|
||||
|
||||
for (auto& picture : input.getAttachedPictures(2))
|
||||
{
|
||||
Image::Image image;
|
||||
Image image;
|
||||
|
||||
if (image.load(picture.data))
|
||||
return image;
|
||||
@@ -93,12 +104,12 @@ getFromAvMediaFile(const Av::MediaFile& input)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Image::Image>
|
||||
std::optional<Image>
|
||||
Grabber::getFromDirectory(const std::filesystem::path& p) const
|
||||
{
|
||||
for (auto coverPath : getCoverPaths(p))
|
||||
{
|
||||
Image::Image image;
|
||||
Image image;
|
||||
|
||||
if (image.load(coverPath))
|
||||
return image;
|
||||
@@ -143,7 +154,7 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<Image::Image>
|
||||
std::optional<Image>
|
||||
Grabber::getFromTrack(const std::filesystem::path& p) const
|
||||
{
|
||||
try
|
||||
@@ -159,12 +170,12 @@ Grabber::getFromTrack(const std::filesystem::path& p) const
|
||||
}
|
||||
}
|
||||
|
||||
Image::Image
|
||||
Image
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
std::optional<Image::Image> cover;
|
||||
std::optional<Image> cover;
|
||||
|
||||
bool hasCover {};
|
||||
bool isMultiDisc {};
|
||||
@@ -200,16 +211,16 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st
|
||||
if (!cover)
|
||||
cover = getDefaultCover(size);
|
||||
else
|
||||
cover->scale(Image::Geometry {size, size});
|
||||
cover->scale(Geometry {size, size});
|
||||
|
||||
return *cover;
|
||||
}
|
||||
|
||||
|
||||
Image::Image
|
||||
Image
|
||||
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, std::size_t size)
|
||||
{
|
||||
std::optional<Image::Image> cover;
|
||||
std::optional<Image> cover;
|
||||
|
||||
std::optional<Database::IdType> trackId;
|
||||
{
|
||||
@@ -230,26 +241,26 @@ Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId,
|
||||
if (!cover)
|
||||
cover = getDefaultCover(size);
|
||||
else
|
||||
cover->scale(Image::Geometry {size, size});
|
||||
cover->scale(Geometry {size, size});
|
||||
|
||||
return *cover;
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Image::Format format, std::size_t size)
|
||||
Grabber::getFromTrack(Database::Session& session, Database::IdType trackId, Format format, std::size_t width)
|
||||
{
|
||||
const Image::Image cover {getFromTrack(session, trackId, size)};
|
||||
const Image cover {getFromTrack(session, trackId, width)};
|
||||
|
||||
assert(format == Image::Format::JPEG);
|
||||
assert(format == Format::JPEG);
|
||||
return cover.save(format);
|
||||
}
|
||||
|
||||
std::vector<uint8_t>
|
||||
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size)
|
||||
Grabber::getFromRelease(Database::Session& session, Database::IdType releaseId, Format format, std::size_t width)
|
||||
{
|
||||
const Image::Image cover {getFromRelease(session, releaseId, size)};
|
||||
const Image cover {getFromRelease(session, releaseId, width)};
|
||||
|
||||
assert(format == Image::Format::JPEG);
|
||||
assert(format == Format::JPEG);
|
||||
return cover.save(format);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "Image.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace CoverArt
|
||||
{
|
||||
|
||||
class Grabber : public IGrabber
|
||||
{
|
||||
public:
|
||||
Grabber(const std::filesystem::path& execPath);
|
||||
~Grabber();
|
||||
|
||||
Grabber(const Grabber&) = delete;
|
||||
Grabber& operator=(const Grabber&) = delete;
|
||||
Grabber(Grabber&&) = delete;
|
||||
Grabber& operator=(Grabber&&) = delete;
|
||||
|
||||
void setDefaultCover(const std::filesystem::path& defaultCoverPath) override;
|
||||
|
||||
std::vector<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) override;
|
||||
std::vector<uint8_t> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) override;
|
||||
|
||||
private:
|
||||
|
||||
Image getFromTrack(Database::Session& dbSession, Database::IdType trackId, std::size_t size);
|
||||
Image getFromRelease(Database::Session& dbSession, Database::IdType releaseId, std::size_t size);
|
||||
|
||||
std::optional<Image> getFromTrack(const std::filesystem::path& path) const;
|
||||
std::vector<std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
|
||||
std::optional<Image> getFromDirectory(const std::filesystem::path& path) const;
|
||||
Image getDefaultCover(std::size_t size);
|
||||
|
||||
Image _defaultCover;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::map<std::size_t /* size */, Image> _defaultCovers;
|
||||
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
static inline const std::size_t _maxFileSize {10000000};
|
||||
static inline const std::vector<std::filesystem::path> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -21,10 +21,23 @@
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Image {
|
||||
namespace CoverArt {
|
||||
|
||||
void
|
||||
init(const std::filesystem::path& path)
|
||||
{
|
||||
Magick::InitializeMagick(path.string().c_str());
|
||||
}
|
||||
|
||||
void
|
||||
deinit()
|
||||
{
|
||||
MagickCore::MagickCoreTerminus();
|
||||
}
|
||||
|
||||
static
|
||||
std::string format_to_magick(Format format)
|
||||
std::string
|
||||
formatToMagick(Format format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
@@ -34,7 +47,8 @@ std::string format_to_magick(Format format)
|
||||
return "JPEG";
|
||||
}
|
||||
|
||||
std::string format_to_mimeType(Format format)
|
||||
std::string
|
||||
formatToMimeType(Format format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
@@ -44,18 +58,13 @@ std::string format_to_mimeType(Format format)
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
void
|
||||
init(const char *path)
|
||||
{
|
||||
Magick::InitializeMagick(path);
|
||||
}
|
||||
|
||||
bool
|
||||
Image::load(const std::vector<unsigned char>& rawData)
|
||||
{
|
||||
try
|
||||
{
|
||||
Magick::Blob blob(&rawData[0], rawData.size());
|
||||
Magick::Blob blob {&rawData[0], rawData.size()};
|
||||
_image.read(blob);
|
||||
|
||||
return true;
|
||||
@@ -116,9 +125,9 @@ Image::save(Format format) const
|
||||
|
||||
try
|
||||
{
|
||||
Magick::Image outputImage(_image);
|
||||
Magick::Image outputImage {_image};
|
||||
|
||||
outputImage.magick( format_to_magick(format));
|
||||
outputImage.magick(formatToMagick(format));
|
||||
|
||||
Magick::Blob blob;
|
||||
outputImage.write(&blob);
|
||||
@@ -135,4 +144,5 @@ Image::save(Format format) const
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Image
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -24,44 +24,34 @@
|
||||
|
||||
#include <Magick++.h>
|
||||
|
||||
namespace Image
|
||||
#include "cover/CoverArt.hpp"
|
||||
|
||||
namespace CoverArt
|
||||
{
|
||||
|
||||
enum class Format
|
||||
{
|
||||
JPEG,
|
||||
};
|
||||
void init(const std::filesystem::path& path);
|
||||
void deinit();
|
||||
|
||||
std::string format_to_mimeType(Format format);
|
||||
class Image
|
||||
{
|
||||
public:
|
||||
|
||||
void init(const char *path);
|
||||
// input
|
||||
bool load(const std::vector<unsigned char>& rawData);
|
||||
bool load(const std::filesystem::path& p);
|
||||
|
||||
struct Geometry
|
||||
{
|
||||
std::size_t width;
|
||||
std::size_t height;
|
||||
};
|
||||
Geometry getSize() const;
|
||||
|
||||
class Image
|
||||
{
|
||||
public:
|
||||
// Operations
|
||||
bool scale(Geometry geometry);
|
||||
|
||||
// input
|
||||
bool load(const std::vector<unsigned char>& rawData);
|
||||
bool load(const std::filesystem::path& p);
|
||||
// output
|
||||
std::vector<uint8_t> save(Format format) const;
|
||||
|
||||
Geometry getSize() const;
|
||||
|
||||
// Operations
|
||||
bool scale(Geometry geometry);
|
||||
|
||||
// output
|
||||
std::vector<uint8_t> save(Format format) const;
|
||||
|
||||
private:
|
||||
Magick::Image _image;
|
||||
};
|
||||
private:
|
||||
Magick::Image _image;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Image
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,23 +20,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
class MediaScannerAddon
|
||||
namespace CoverArt
|
||||
{
|
||||
public:
|
||||
|
||||
virtual void refreshSettings() = 0;
|
||||
virtual void requestStop() = 0;
|
||||
virtual void preScanComplete() = 0;
|
||||
enum class Format
|
||||
{
|
||||
JPEG,
|
||||
};
|
||||
std::string formatToMimeType(Format format);
|
||||
|
||||
virtual void trackAdded(Database::IdType trackId) = 0;
|
||||
virtual void trackToRemove(Database::IdType trackId) = 0;
|
||||
virtual void trackUpdated(Database::IdType trackId) = 0;
|
||||
|
||||
};
|
||||
|
||||
} // ns Scanner
|
||||
struct Geometry
|
||||
{
|
||||
std::size_t width;
|
||||
std::size_t height;
|
||||
};
|
||||
|
||||
}
|
||||
+17
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,22 +19,30 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "cover/CoverArt.hpp"
|
||||
|
||||
namespace Database {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Similarity {
|
||||
namespace CoverArt {
|
||||
|
||||
namespace ClusterSearcher
|
||||
class IGrabber
|
||||
{
|
||||
std::vector<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount);
|
||||
std::vector<Database::IdType> getSimilarTracks(Database::Session& session, const std::set<Database::IdType>& tracksId, std::size_t maxCount);
|
||||
std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount);
|
||||
std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount);
|
||||
public:
|
||||
virtual ~IGrabber() = default;
|
||||
|
||||
virtual void setDefaultCover(const std::filesystem::path& defaultCoverPath) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> getFromTrack(Database::Session& dbSession, Database::IdType trackId, Format format, std::size_t width) = 0;
|
||||
virtual std::vector<uint8_t> getFromRelease(Database::Session& dbSession, Database::IdType releaseId, Format format, std::size_t width) = 0;
|
||||
};
|
||||
|
||||
} // namespace Similarity
|
||||
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath);
|
||||
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
add_library(lmsdatabase SHARED
|
||||
impl/Artist.cpp
|
||||
impl/Cluster.cpp
|
||||
impl/Db.cpp
|
||||
impl/TrackArtistLink.cpp
|
||||
impl/TrackFeatures.cpp
|
||||
impl/TrackList.cpp
|
||||
impl/Release.cpp
|
||||
impl/ScanSettings.cpp
|
||||
impl/Session.cpp
|
||||
impl/SessionPool.cpp
|
||||
impl/SqlQuery.cpp
|
||||
impl/Track.cpp
|
||||
impl/TrackBookmark.cpp
|
||||
impl/User.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsdatabase INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsdatabase PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmsdatabase PRIVATE
|
||||
wtdbosqlite3
|
||||
)
|
||||
|
||||
target_link_libraries(lmsdatabase PUBLIC
|
||||
lmsutils
|
||||
wtdbo
|
||||
)
|
||||
|
||||
install(TARGETS lmsdatabase DESTINATION lib)
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "Artist.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Cluster.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "User.hpp"
|
||||
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -104,6 +104,21 @@ Artist::getAllOrphans(Session& session)
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Artist::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
("SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Session& session,
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Cluster.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "ScanSettings.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Track.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Db.hpp"
|
||||
#include "database/Db.hpp"
|
||||
|
||||
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "User.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Release.hpp"
|
||||
#include "database/Release.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Cluster.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "User.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -252,6 +252,21 @@ Release::getByFilter(Session& session,
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
("SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalTrackNumber(void) const
|
||||
{
|
||||
@@ -17,15 +17,16 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ScanSettings.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "Cluster.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -62,11 +63,11 @@ ScanSettings::get(Session& session)
|
||||
return session.getDboSession().find<ScanSettings>();
|
||||
}
|
||||
|
||||
std::set<std::filesystem::path>
|
||||
std::unordered_set<std::filesystem::path>
|
||||
ScanSettings::getAudioFileExtensions() const
|
||||
{
|
||||
auto extensions = StringUtils::splitString(_audioFileExtensions, " ");
|
||||
return std::set<std::filesystem::path>(std::cbegin(extensions), std::cend(extensions));
|
||||
return std::unordered_set<std::filesystem::path>(std::cbegin(extensions), std::cend(extensions));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Session.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -26,17 +26,17 @@
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Cluster.hpp"
|
||||
#include "Db.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "ScanSettings.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "TrackBookmark.hpp"
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "TrackList.hpp"
|
||||
#include "TrackFeatures.hpp"
|
||||
#include "User.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -118,7 +118,7 @@ Session::doDatabaseMigrationIfNeeded()
|
||||
{
|
||||
_session.execute("DROP TABLE similarity_settings");
|
||||
_session.execute("DROP TABLE similarity_settings_feature");
|
||||
_session.execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::SimilarityEngineType::Clusters)) + ")");
|
||||
_session.execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
|
||||
}
|
||||
else if (version == 8)
|
||||
{
|
||||
@@ -17,13 +17,13 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SessionPool.hpp"
|
||||
#include "database/SessionPool.hpp"
|
||||
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Session.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
SessionPool::SessionPool(Db& database, std::size_t maxSessionCount)
|
||||
@@ -17,17 +17,17 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Track.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Cluster.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "TrackFeatures.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
|
||||
namespace Database {
|
||||
@@ -163,6 +163,19 @@ Track::getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit)
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
("SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Track::getClusters(void) const
|
||||
{
|
||||
@@ -263,7 +276,7 @@ Track::getByFilter(Session& session,
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getSimilarTracks(Session& session,
|
||||
const std::set<IdType>& tracks,
|
||||
const std::unordered_set<IdType>& tracks,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size)
|
||||
{
|
||||
@@ -17,11 +17,11 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackArtistLink.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackBookmark.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "User.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TrackFeatures.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -16,19 +16,18 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "TrackList.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <random>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "Artist.hpp"
|
||||
#include "Cluster.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "User.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "User.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "Artist.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "Session.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "TrackList.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -68,6 +68,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastAdded(Session& session, Wt::WDateTime after, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
// Accessors
|
||||
const std::string& getName(void) const { return _name; }
|
||||
@@ -66,6 +66,7 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
bool& moreExpected);
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Track>> getTracks(const std::set<IdType>& clusters = std::set<IdType>()) const;
|
||||
std::size_t getTracksCount() const;
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WTime.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class ClusterType;
|
||||
@@ -43,7 +45,7 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
};
|
||||
|
||||
// Do not modify values (just add)
|
||||
enum class SimilarityEngineType
|
||||
enum class RecommendationEngineType
|
||||
{
|
||||
Clusters = 0,
|
||||
Features,
|
||||
@@ -59,8 +61,8 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<Wt::Dbo::ptr<ClusterType>> getClusterTypes() const;
|
||||
std::set<std::filesystem::path> getAudioFileExtensions() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
std::unordered_set<std::filesystem::path> getAudioFileExtensions() const;
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
|
||||
// Setters
|
||||
void addAudioFileExtension(const std::filesystem::path& ext);
|
||||
@@ -68,7 +70,7 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; }
|
||||
void incScanVersion();
|
||||
|
||||
template<class Action>
|
||||
@@ -79,7 +81,7 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _similarityEngineType,"similarity_engine_type");
|
||||
Wt::Dbo::field(a, _recommendationEngineType,"similarity_engine_type");
|
||||
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
|
||||
}
|
||||
|
||||
@@ -89,7 +91,7 @@ class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime {0,0,0};
|
||||
UpdatePeriod _updatePeriod {UpdatePeriod::Never};
|
||||
SimilarityEngineType _similarityEngineType {SimilarityEngineType::Clusters};
|
||||
RecommendationEngineType _recommendationEngineType {RecommendationEngineType::Clusters};
|
||||
std::string _audioFileExtensions {".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus"};
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
|
||||
};
|
||||
@@ -67,8 +67,8 @@ class Session
|
||||
Session& operator=(const Session&) = delete;
|
||||
Session& operator=(Session&&) = delete;
|
||||
|
||||
UniqueTransaction createUniqueTransaction();
|
||||
SharedTransaction createSharedTransaction();
|
||||
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
||||
[[nodiscard]] SharedTransaction createSharedTransaction();
|
||||
|
||||
void checkUniqueLocked();
|
||||
void checkSharedLocked();
|
||||
@@ -22,8 +22,9 @@
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
@@ -58,7 +59,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> getSimilarTracks(Session& session,
|
||||
const std::set<IdType>& trackIds,
|
||||
const std::unordered_set<IdType>& trackIds,
|
||||
std::optional<std::size_t> offset = {},
|
||||
std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getByClusters(Session& session,
|
||||
@@ -78,6 +79,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
// Create utility
|
||||
static pointer create(Session& session, const std::filesystem::path& p);
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
add_library(lmsmetadata SHARED
|
||||
impl/AvFormatParser.cpp
|
||||
impl/TagLibParser.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsmetadata INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsmetadata PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PRIVATE
|
||||
lmsav
|
||||
tag
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PUBLIC
|
||||
lmsutils
|
||||
)
|
||||
|
||||
install(TARGETS lmsmetadata DESTINATION lib)
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AvFormat.hpp"
|
||||
#include "metadata/AvFormatParser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
@@ -128,7 +127,7 @@ getArtists(const MetadataMap& metadataMap)
|
||||
}
|
||||
|
||||
std::optional<Track>
|
||||
AvFormat::parse(const std::filesystem::path& p, bool debug)
|
||||
AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
Track track;
|
||||
|
||||
@@ -142,7 +141,7 @@ AvFormat::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
for (auto stream : mediaFile.getStreamInfo())
|
||||
{
|
||||
MetaData::AudioStream audioStream {.bitRate = static_cast<unsigned>(stream.bitrate)};
|
||||
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)};
|
||||
track.audioStreams.emplace_back(audioStream);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagLibParser.hpp"
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
@@ -180,7 +180,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
track.duration = std::chrono::milliseconds {properties->length() * 1000};
|
||||
|
||||
MetaData::AudioStream audioStream {.bitRate = static_cast<unsigned>(properties->bitrate() * 1000)};
|
||||
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
||||
track.audioStreams = {std::move(audioStream)};
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MetaData.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
// Parse that makes use of AvFormat
|
||||
class AvFormat : public Parser
|
||||
class AvFormatParser : public IParser
|
||||
{
|
||||
public:
|
||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
//#include "utils/Utils.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace MetaData
|
||||
@@ -73,7 +72,7 @@ namespace MetaData
|
||||
std::string copyrightURL;
|
||||
};
|
||||
|
||||
class Parser
|
||||
class IParser
|
||||
{
|
||||
public:
|
||||
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
||||
@@ -19,13 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MetaData.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
// Parse that makes use of AvFormat
|
||||
class TagLibParser : public Parser
|
||||
class TagLibParser : public IParser
|
||||
{
|
||||
public:
|
||||
std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) override;
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
add_library(lmsrecommendation SHARED
|
||||
impl/clusters/ClustersClassifier.cpp
|
||||
impl/features/FeaturesClassifierCache.cpp
|
||||
impl/features/FeaturesClassifier.cpp
|
||||
impl/features/FeaturesDefs.cpp
|
||||
impl/Engine.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsrecommendation INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsrecommendation PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmsrecommendation PRIVATE
|
||||
lmsdatabase
|
||||
lmssom
|
||||
wt
|
||||
)
|
||||
|
||||
install(TARGETS lmsrecommendation DESTINATION lib)
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "Engine.hpp"
|
||||
|
||||
#include "recommendation/ClustersClassifierCreator.hpp"
|
||||
#include "recommendation/FeaturesClassifierCreator.hpp"
|
||||
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
std::unique_ptr<IEngine>
|
||||
createEngine(Database::Db& db)
|
||||
{
|
||||
return std::make_unique<Engine>(db);
|
||||
}
|
||||
|
||||
Engine::Engine(Database::Db& db)
|
||||
: _dbSession {db}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Engine::start()
|
||||
{
|
||||
assert(!_running);
|
||||
_running = true;
|
||||
|
||||
requestReloadInternal(false);
|
||||
|
||||
_ioService.start();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::stop()
|
||||
{
|
||||
assert(_running);
|
||||
_running = false;
|
||||
|
||||
cancelPendingClassifiers();
|
||||
|
||||
_ioService.stop();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::requestReload()
|
||||
{
|
||||
requestReloadInternal(true);
|
||||
}
|
||||
|
||||
void
|
||||
Engine::requestReloadInternal(bool databaseChanged)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Reload requested...";
|
||||
|
||||
_ioService.post([=]()
|
||||
{
|
||||
reload(databaseChanged);
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount)
|
||||
{
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierName)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
res = itClassifier->second->getSimilarTracksFromTrackList(session, trackListId, maxCount);
|
||||
if (!res.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<Database::IdType>& trackIds, std::size_t maxCount)
|
||||
{
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierName)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
res = itClassifier->second->getSimilarTracks(dbSession, trackIds, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifierName << "'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount)
|
||||
{
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierName)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
res = itClassifier->second->getSimilarReleases(dbSession, releaseId, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifierName << "'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount)
|
||||
{
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierName)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
res = itClassifier->second->getSimilarArtists(dbSession, artistId, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifierName << "'";
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
Engine::reload(bool databaseChanged)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
|
||||
|
||||
const ScanSettings::RecommendationEngineType engineType {[&]()
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
|
||||
return ScanSettings::get(_dbSession)->getRecommendationEngineType();
|
||||
}()};
|
||||
|
||||
clearClassifiers();
|
||||
|
||||
switch (engineType)
|
||||
{
|
||||
case ScanSettings::RecommendationEngineType::Features:
|
||||
{
|
||||
auto clustersClassifier {createClustersClassifier()};
|
||||
auto featuresClassifier {createFeaturesClassifier()};
|
||||
|
||||
setClassifierPriorities({featuresClassifier->getName(), clustersClassifier->getName()});
|
||||
|
||||
initAndAddClassifier(std::move(clustersClassifier), databaseChanged); // init first since faster
|
||||
initAndAddClassifier(std::move(featuresClassifier), databaseChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
case ScanSettings::RecommendationEngineType::Clusters:
|
||||
auto clustersClassifier {createClustersClassifier()};
|
||||
|
||||
setClassifierPriorities({clustersClassifier->getName()});
|
||||
|
||||
initAndAddClassifier(std::move(clustersClassifier), databaseChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines reloaded!";
|
||||
|
||||
_sigReloaded.emit();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::setClassifierPriorities(std::initializer_list<std::string_view> classifierPriorities)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_classifierPriorities.clear();
|
||||
std::transform(std::cbegin(classifierPriorities), std::cend(classifierPriorities), std::back_inserter(_classifierPriorities), [](std::string_view name) { return std::string {name}; });
|
||||
}
|
||||
|
||||
void
|
||||
Engine::clearClassifiers()
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_classifiers.clear();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::initAndAddClassifier(std::unique_ptr<IClassifier> classifier, bool databaseChanged)
|
||||
{
|
||||
PendingClassifierHandler pendingClassifier {*this, *classifier.get()};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'...";
|
||||
bool res {classifier->init(_dbSession, databaseChanged)};
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE");
|
||||
|
||||
if (res)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_classifiers.emplace(classifier->getName(), std::move(classifier));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Engine::cancelPendingClassifiers()
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
for (IClassifier* classifier : _pendingClassifiers)
|
||||
classifier->requestCancelInit();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::addPendingClassifier(IClassifier& classifier)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_pendingClassifiers.insert(&classifier);
|
||||
}
|
||||
|
||||
void
|
||||
Engine::removePendingClassifier(IClassifier& classifier)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_pendingClassifiers.erase(&classifier);
|
||||
}
|
||||
|
||||
} // ns Similarity
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <shared_mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WIOService.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "recommendation/IClassifier.hpp"
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class Engine : public IEngine
|
||||
{
|
||||
public:
|
||||
Engine(Database::Db& db);
|
||||
|
||||
private:
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
void requestReload() override;
|
||||
Wt::Signal<>& reloaded() override { return _sigReloaded; }
|
||||
|
||||
std::vector<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) override;
|
||||
std::vector<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) override;
|
||||
std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) override;
|
||||
std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) override;
|
||||
|
||||
|
||||
void requestReloadInternal(bool databaseChanged);
|
||||
void reload(bool databaseChanged);
|
||||
|
||||
void setClassifierPriorities(std::initializer_list<std::string_view> classifierNames);
|
||||
void clearClassifiers();
|
||||
void initAndAddClassifier(std::unique_ptr<IClassifier> classifier, bool databaseChanged);
|
||||
|
||||
class PendingClassifierHandler
|
||||
{
|
||||
public:
|
||||
PendingClassifierHandler(Engine& engine, IClassifier& classifier) : _engine {engine}, _classifier {classifier}
|
||||
{
|
||||
_engine.addPendingClassifier(_classifier);
|
||||
}
|
||||
|
||||
~PendingClassifierHandler()
|
||||
{
|
||||
_engine.removePendingClassifier(_classifier);
|
||||
}
|
||||
|
||||
private:
|
||||
Engine& _engine;
|
||||
IClassifier& _classifier;
|
||||
};
|
||||
|
||||
void cancelPendingClassifiers();
|
||||
void addPendingClassifier(IClassifier& classifier);
|
||||
void removePendingClassifier(IClassifier& classifier);
|
||||
|
||||
bool _running {};
|
||||
Wt::WIOService _ioService;
|
||||
Database::Session _dbSession;
|
||||
Wt::Signal<> _sigReloaded;
|
||||
|
||||
std::shared_mutex _classifiersMutex;
|
||||
std::map<std::string, std::unique_ptr<IClassifier>> _classifiers;
|
||||
std::vector<std::string> _classifierPriorities; // ordered by priority
|
||||
std::unordered_set<IClassifier*> _pendingClassifiers;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
+12
-12
@@ -17,10 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SimilarityClusterSearcher.hpp"
|
||||
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include "ClustersClassifier.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
@@ -29,11 +26,15 @@
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
namespace Similarity {
|
||||
namespace ClusterSearcher {
|
||||
namespace Recommendation {
|
||||
|
||||
std::unique_ptr<IClassifier> createClustersClassifier()
|
||||
{
|
||||
return std::make_unique<ClusterClassifier>();
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
getSimilarTracks(Database::Session& dbSession, const std::set<Database::IdType>& trackIds, std::size_t maxCount)
|
||||
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<Database::IdType>& trackIds, std::size_t maxCount) const
|
||||
{
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -46,7 +47,7 @@ getSimilarTracks(Database::Session& dbSession, const std::set<Database::IdType>&
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount)
|
||||
ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
@@ -65,7 +66,7 @@ getSimilarTracksFromTrackList(Database::Session& session, Database::IdType track
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount)
|
||||
ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
@@ -83,7 +84,7 @@ getSimilarReleases(Database::Session& dbSession, Database::IdType releaseId, std
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount)
|
||||
ClusterClassifier::getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
@@ -100,5 +101,4 @@ getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace ClusterSearcher
|
||||
} // namespace Similarity
|
||||
} // namespace Recommendation
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "recommendation/IClassifier.hpp"
|
||||
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
|
||||
class ClusterClassifier : public IClassifier
|
||||
{
|
||||
public:
|
||||
ClusterClassifier() = default;
|
||||
ClusterClassifier(const ClusterClassifier&) = delete;
|
||||
ClusterClassifier(ClusterClassifier&&) = delete;
|
||||
ClusterClassifier& operator=(const ClusterClassifier&) = delete;
|
||||
ClusterClassifier& operator=(ClusterClassifier&&) = delete;
|
||||
|
||||
private:
|
||||
|
||||
std::string_view getName() const { return "Clusters"; }
|
||||
|
||||
bool init(Database::Session&, bool) override {return true;}
|
||||
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 override;
|
||||
std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Recommendation
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "FeaturesClassifier.hpp"
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "som/DataNormalizer.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
std::unique_ptr<IClassifier> createFeaturesClassifier()
|
||||
{
|
||||
return std::make_unique<FeaturesClassifier>();
|
||||
}
|
||||
|
||||
const FeatureSettingsMap&
|
||||
FeaturesClassifier::getDefaultTrainFeatureSettings()
|
||||
{
|
||||
static const FeatureSettingsMap defaultTrainFeatureSettings
|
||||
{
|
||||
{ "lowlevel.spectral_energyband_high.mean", {1}},
|
||||
{ "lowlevel.spectral_rolloff.median", {1}},
|
||||
{ "lowlevel.spectral_contrast_valleys.var", {1}},
|
||||
{ "lowlevel.erbbands.mean", {1}},
|
||||
{ "lowlevel.gfcc.mean", {1}},
|
||||
};
|
||||
|
||||
return defaultTrainFeatureSettings;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<FeatureValuesMap>
|
||||
getTrackFeatureValues(FeaturesClassifier::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
return func(trackId, featureNames);
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<FeatureValuesMap>
|
||||
getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
auto func = [&](Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames)
|
||||
{
|
||||
std::optional<FeatureValuesMap> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
||||
if (!track)
|
||||
return res;
|
||||
|
||||
res = track->getTrackFeatures()->getFeatureValuesMap(featureNames);
|
||||
if (res->empty())
|
||||
res.reset();
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
return getTrackFeatureValues(func, trackId, featureNames);
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<SOM::InputVector>
|
||||
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
||||
{
|
||||
std::size_t i {};
|
||||
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
|
||||
for (const auto& [featureName, values] : featureValuesMap)
|
||||
{
|
||||
if (values.size() != getFeatureDef(featureName).nbDimensions)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
|
||||
res.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
for (double val : values)
|
||||
(*res)[i++] = val;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
SOM::InputVector
|
||||
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
|
||||
{
|
||||
SOM::InputVector weights {nbDimensions};
|
||||
std::size_t index {};
|
||||
for (const auto& [featureName, featureSettings] : featureSettingsMap)
|
||||
{
|
||||
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
|
||||
|
||||
for (std::size_t i {}; i < featureNbDimensions; ++i)
|
||||
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
|
||||
}
|
||||
|
||||
assert(index == nbDimensions);
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSettings& trainSettings)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
|
||||
|
||||
std::unordered_set<FeatureName> featureNames;
|
||||
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
|
||||
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
|
||||
|
||||
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
|
||||
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
||||
|
||||
std::vector<Database::IdType> trackIds;
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features...";
|
||||
trackIds = Database::Track::getAllIdsWithFeatures(session);
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)";
|
||||
}
|
||||
|
||||
std::vector<SOM::InputVector> samples;
|
||||
std::vector<Database::IdType> samplesTrackIds;
|
||||
|
||||
samples.reserve(trackIds.size());
|
||||
samplesTrackIds.reserve(trackIds.size());
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
||||
for (Database::IdType trackId : trackIds)
|
||||
{
|
||||
if (_initCancelled)
|
||||
return false;
|
||||
|
||||
std::optional<FeatureValuesMap> featureValuesMap;
|
||||
|
||||
if (_featuresFetchFunc)
|
||||
featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames);
|
||||
else
|
||||
featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames);
|
||||
|
||||
if (!featureValuesMap)
|
||||
continue;
|
||||
|
||||
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)};
|
||||
if (!inputVector)
|
||||
continue;
|
||||
|
||||
samples.emplace_back(std::move(*inputVector));
|
||||
samplesTrackIds.emplace_back(trackId);
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
|
||||
|
||||
if (samples.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
|
||||
return false;
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
|
||||
SOM::DataNormalizer dataNormalizer {nbDimensions};
|
||||
|
||||
dataNormalizer.computeNormalizationFactors(samples);
|
||||
for (auto& sample : samples)
|
||||
dataNormalizer.normalizeData(sample);
|
||||
|
||||
const SOM::Coordinate size {static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
|
||||
|
||||
SOM::Network network {size, size, nbDimensions};
|
||||
|
||||
SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
|
||||
network.setDataWeights(weights);
|
||||
|
||||
auto progressIndicator{[](const auto& iter)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
|
||||
}};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
|
||||
network.train(samples, trainSettings.iterationCount, progressIndicator);
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
|
||||
|
||||
if (_initCancelled)
|
||||
return false;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
||||
ObjectPositions trackPositions;
|
||||
for (std::size_t i {}; i < samples.size(); ++i)
|
||||
{
|
||||
if (_initCancelled)
|
||||
return false;
|
||||
|
||||
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
|
||||
|
||||
trackPositions[samplesTrackIds[i]].insert(position);
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
|
||||
|
||||
return init(session, std::move(network), std::move(trackPositions));
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::initFromCache(Database::Session& session, const FeaturesClassifierCache& cache)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
|
||||
|
||||
return init(session, std::move(cache._network), cache._trackPositions);
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const
|
||||
{
|
||||
const std::unordered_set<Database::IdType> trackIds {[&]() -> std::unordered_set<Database::IdType>
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)};
|
||||
if (trackList)
|
||||
{
|
||||
const std::vector<Database::IdType> orderedTrackIds {trackList->getTrackIds()};
|
||||
return std::unordered_set<Database::IdType> {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)};
|
||||
}
|
||||
|
||||
return {};
|
||||
}()};
|
||||
|
||||
return getSimilarTracks(session, trackIds, maxCount);
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
FeaturesClassifier::getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksIds, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> similarTrackIds {getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount)};
|
||||
|
||||
if (!similarTrackIds.empty())
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||
[&](Database::IdType trackId) { return Database::Track::getById(session, trackId) == Database::Track::pointer {}; }),
|
||||
std::cend(similarTrackIds));
|
||||
}
|
||||
|
||||
return similarTrackIds;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
FeaturesClassifier::getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> similarReleaseIds {getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount)};
|
||||
|
||||
if (!similarReleaseIds.empty())
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||
[&](Database::IdType releaseId) { return Database::Release::getById(session, releaseId) == Database::Release::pointer {}; }),
|
||||
std::cend(similarReleaseIds));
|
||||
}
|
||||
|
||||
return similarReleaseIds;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
FeaturesClassifier::getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> similarArtistIds {getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount)};
|
||||
|
||||
if (!similarArtistIds.empty())
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
similarArtistIds.erase(std::remove_if(std::begin(similarArtistIds), std::end(similarArtistIds),
|
||||
[&](Database::IdType artistId) { return Database::Artist::getById(session, artistId) == Database::Artist::pointer {}; }),
|
||||
std::cend(similarArtistIds));
|
||||
}
|
||||
|
||||
return similarArtistIds;
|
||||
}
|
||||
|
||||
FeaturesClassifierCache
|
||||
FeaturesClassifier::toCache() const
|
||||
{
|
||||
return FeaturesClassifierCache {*_network, _trackPositions};
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::init(Database::Session& session, bool databaseChanged)
|
||||
{
|
||||
if (databaseChanged)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Database changed: invidating cache";
|
||||
FeaturesClassifierCache::invalidate();
|
||||
}
|
||||
|
||||
std::optional<FeaturesClassifierCache> cache {FeaturesClassifierCache::read()};
|
||||
if (cache)
|
||||
return initFromCache(session, *cache);
|
||||
|
||||
TrainSettings trainSettings;
|
||||
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
||||
|
||||
bool res {initFromTraining(session, trainSettings)};
|
||||
if (res)
|
||||
toCache().write();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesClassifier::requestCancelInit()
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
|
||||
_initCancelled = true;
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesClassifier::init(Database::Session& session,
|
||||
SOM::Network network,
|
||||
const ObjectPositions& tracksPosition)
|
||||
{
|
||||
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
|
||||
|
||||
const SOM::Coordinate width {network.getWidth()};
|
||||
const SOM::Coordinate height {network.getHeight()};
|
||||
|
||||
_artistsMap = MatrixOfObjects {width, height};
|
||||
_releasesMap = MatrixOfObjects {width, height};
|
||||
_tracksMap = MatrixOfObjects {width, height};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
||||
|
||||
for (auto itTrackCoord : tracksPosition)
|
||||
{
|
||||
if (_initCancelled)
|
||||
return false;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
Database::IdType trackId {itTrackCoord.first};
|
||||
const std::unordered_set<SOM::Position>& positionSet {itTrackCoord.second};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
for (const SOM::Position& position : positionSet)
|
||||
{
|
||||
_tracksMap[position].insert(trackId);
|
||||
_trackPositions[trackId].insert(position);
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
_releasePositions[track->getRelease().id()].insert(position);
|
||||
_releasesMap[position].insert(track->getRelease().id());
|
||||
}
|
||||
for (const auto& artist : track->getArtists())
|
||||
{
|
||||
_artistPositions[artist.id()].insert(position);
|
||||
_artistsMap[position].insert(artist.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_network = std::make_unique<SOM::Network>(std::move(network));
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully initialized!";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unordered_set<SOM::Position>
|
||||
FeaturesClassifier::getMatchingRefVectorsPosition(const std::unordered_set<Database::IdType>& ids, const ObjectPositions& objectPositions)
|
||||
{
|
||||
std::unordered_set<SOM::Position> res;
|
||||
|
||||
if (ids.empty())
|
||||
return res;
|
||||
|
||||
for (auto id : ids)
|
||||
{
|
||||
auto it = objectPositions.find(id);
|
||||
if (it == objectPositions.end())
|
||||
continue;
|
||||
|
||||
for (const auto& position : it->second)
|
||||
res.insert(position);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unordered_set<Database::IdType>
|
||||
FeaturesClassifier::getObjectsIds(const std::unordered_set<SOM::Position>& positionSet, const MatrixOfObjects& objectsMap)
|
||||
{
|
||||
std::unordered_set<Database::IdType> res;
|
||||
|
||||
for (const auto& position : positionSet)
|
||||
{
|
||||
for (auto id : objectsMap.get(position))
|
||||
res.insert(id);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::IdType>
|
||||
FeaturesClassifier::getSimilarObjects(const std::unordered_set<Database::IdType>& ids,
|
||||
const MatrixOfObjects& objectsMap,
|
||||
const ObjectPositions& objectPosition,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
std::vector<Database::IdType> res;
|
||||
|
||||
std::unordered_set<SOM::Position> searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)};
|
||||
if (searchedRefVectorsPosition.empty())
|
||||
return res;
|
||||
|
||||
while (1)
|
||||
{
|
||||
std::unordered_set<Database::IdType> closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)};
|
||||
|
||||
// Remove objects that are already in input or already reported
|
||||
for (auto id : ids)
|
||||
closestObjectIds.erase(id);
|
||||
|
||||
{
|
||||
std::vector<Database::IdType> objectIdsToAdd {std::cbegin(closestObjectIds), std::cend(closestObjectIds)};
|
||||
Random::shuffleContainer(objectIdsToAdd );
|
||||
std::copy(std::cbegin(objectIdsToAdd), std::cend(objectIdsToAdd), std::back_inserter(res));
|
||||
}
|
||||
|
||||
if (res.size() > maxCount)
|
||||
res.resize(maxCount);
|
||||
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
|
||||
// If there is not enough objects, try again with closest neighbour until there is too much distance
|
||||
const std::optional<SOM::Position> closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)};
|
||||
if (!closestRefVectorPosition)
|
||||
break;
|
||||
|
||||
searchedRefVectorsPosition.insert(closestRefVectorPosition.value());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // ns Recommendation
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <unordered_map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "recommendation/IClassifier.hpp"
|
||||
#include "som/DataNormalizer.hpp"
|
||||
#include "som/Network.hpp"
|
||||
#include "FeaturesClassifierCache.hpp"
|
||||
#include "FeaturesDefs.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
using FeatureWeight = double;
|
||||
|
||||
class FeaturesClassifier : public IClassifier
|
||||
{
|
||||
public:
|
||||
FeaturesClassifier() = default;
|
||||
FeaturesClassifier(const FeaturesClassifier&) = delete;
|
||||
FeaturesClassifier(FeaturesClassifier&&) = delete;
|
||||
FeaturesClassifier& operator=(const FeaturesClassifier&) = delete;
|
||||
FeaturesClassifier& operator=(FeaturesClassifier&&) = delete;
|
||||
|
||||
using FeaturesFetchFunc = std::function<std::optional<std::unordered_map<std::string, std::vector<double>>>(Database::IdType /*trackId*/, const std::unordered_set<std::string>& /*features*/)>;
|
||||
// Default is to retrieve the features from the database (may be slow).
|
||||
// Use this only if you want to train different searchers with some cached data
|
||||
static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; }
|
||||
|
||||
static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
|
||||
private:
|
||||
|
||||
std::string_view getName() const { 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;
|
||||
|
||||
bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
|
||||
|
||||
// Use training (may be very slow)
|
||||
struct TrainSettings
|
||||
{
|
||||
std::size_t iterationCount {10};
|
||||
float sampleCountPerNeuron {4};
|
||||
FeatureSettingsMap featureSettingsMap;
|
||||
};
|
||||
bool initFromTraining(Database::Session& session, const TrainSettings& trainSettings);
|
||||
|
||||
using ObjectPositions = std::unordered_map<Database::IdType, std::unordered_set<SOM::Position>>;
|
||||
using MatrixOfObjects = SOM::Matrix<std::unordered_set<Database::IdType>>;
|
||||
|
||||
bool init(Database::Session& session,
|
||||
SOM::Network network,
|
||||
const ObjectPositions& tracksPosition);
|
||||
|
||||
FeaturesClassifierCache toCache() const;
|
||||
|
||||
static std::unordered_set<SOM::Position> getMatchingRefVectorsPosition(const std::unordered_set<Database::IdType>& ids, const ObjectPositions& objectPositions);
|
||||
static std::unordered_set<Database::IdType> getObjectsIds(const std::unordered_set<SOM::Position>& positionSet, const MatrixOfObjects& objectsMap);
|
||||
|
||||
std::vector<Database::IdType> getSimilarObjects(const std::unordered_set<Database::IdType>& ids,
|
||||
const SOM::Matrix<std::unordered_set<Database::IdType>>& objectsMap,
|
||||
const ObjectPositions& objectPosition,
|
||||
std::size_t maxCount) const;
|
||||
|
||||
bool _initCancelled {};
|
||||
std::unique_ptr<SOM::Network> _network;
|
||||
double _networkRefVectorsDistanceMedian {};
|
||||
|
||||
MatrixOfObjects _artistsMap;
|
||||
ObjectPositions _artistPositions;
|
||||
|
||||
MatrixOfObjects _releasesMap;
|
||||
ObjectPositions _releasePositions;
|
||||
|
||||
MatrixOfObjects _tracksMap;
|
||||
ObjectPositions _trackPositions;
|
||||
|
||||
static inline FeaturesFetchFunc _featuresFetchFunc;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
+29
-35
@@ -17,28 +17,28 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SimilarityFeaturesCache.hpp"
|
||||
#include "FeaturesClassifierCache.hpp"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include "utils/Config.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace Similarity {
|
||||
namespace Recommendation {
|
||||
|
||||
|
||||
static
|
||||
std::filesystem::path getCacheDirectory()
|
||||
{
|
||||
return ServiceProvider<Config>::get()->getPath("working-dir") / "cache" / "features";
|
||||
return ServiceProvider<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||
}
|
||||
|
||||
static std::filesystem::path getCacheNetworkFilePath()
|
||||
{
|
||||
return getCacheDirectory() / "network";
|
||||
};
|
||||
}
|
||||
|
||||
static std::filesystem::path getCacheTrackPositionsFilePath()
|
||||
{
|
||||
@@ -79,26 +79,25 @@ networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
||||
|
||||
boost::property_tree::write_xml(path.string(), root);
|
||||
|
||||
LMS_LOG(SIMILARITY, DEBUG) << "Created network cache";
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
|
||||
return true;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "Cannot create network cache: " << error.what();
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<SOM::Network>
|
||||
createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
FeaturesClassifierCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
LMS_LOG(SIMILARITY, INFO) << "Reading network from cache...";
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
|
||||
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
@@ -132,20 +131,19 @@ createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
res.setRefVector({x, y}, refVector);
|
||||
}
|
||||
|
||||
LMS_LOG(SIMILARITY, INFO) << "Successfully read network from cache";
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
|
||||
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "Cannot read network cache: " << error.what();
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
bool
|
||||
objectPositionToCacheFile(const std::map<Database::IdType, std::set<SOM::Position>>& objectsPosition, std::filesystem::path path)
|
||||
FeaturesClassifierCache::objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -174,24 +172,23 @@ objectPositionToCacheFile(const std::map<Database::IdType, std::set<SOM::Positio
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "Cannot cache object position: " << error.what();
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot cache object position: " << error.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<std::map<Database::IdType, std::set<SOM::Position>>>
|
||||
createObjectPositionsFromCacheFile(std::filesystem::path path)
|
||||
std::optional<FeaturesClassifierCache::ObjectPositions>
|
||||
FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(SIMILARITY, INFO) << "Reading object position from cache...";
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
|
||||
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
|
||||
std::map<Database::IdType, std::set<SOM::Position>> res;
|
||||
ObjectPositions res;
|
||||
|
||||
for (const auto& object : root.get_child("objects"))
|
||||
{
|
||||
@@ -205,44 +202,42 @@ createObjectPositionsFromCacheFile(std::filesystem::path path)
|
||||
}
|
||||
}
|
||||
|
||||
LMS_LOG(SIMILARITY, INFO) << "Successfully read object position from cache";
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache";
|
||||
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "Cannot create object position from cache file: " << error.what();
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesCache::invalidate()
|
||||
FeaturesClassifierCache::invalidate()
|
||||
{
|
||||
std::filesystem::remove(getCacheNetworkFilePath());
|
||||
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
||||
}
|
||||
|
||||
std::optional<FeaturesCache>
|
||||
FeaturesCache::read()
|
||||
std::optional<FeaturesClassifierCache>
|
||||
FeaturesClassifierCache::read()
|
||||
{
|
||||
std::optional<FeaturesCache> res;
|
||||
|
||||
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
|
||||
if (!network)
|
||||
return res;
|
||||
return std::nullopt;
|
||||
|
||||
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
|
||||
if (!trackPositions)
|
||||
return res;
|
||||
return std::nullopt;
|
||||
|
||||
return FeaturesCache{std::move(*network), std::move(*trackPositions)};
|
||||
return FeaturesClassifierCache {std::move(*network), std::move(*trackPositions)};
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesCache::write()
|
||||
FeaturesClassifierCache::write() const
|
||||
{
|
||||
std::filesystem::create_directories(ServiceProvider<Config>::get()->getPath("working-dir") / "cache" / "features");
|
||||
std::filesystem::create_directories(ServiceProvider<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
|
||||
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
||||
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
||||
@@ -251,11 +246,10 @@ FeaturesCache::write()
|
||||
}
|
||||
}
|
||||
|
||||
FeaturesCache::FeaturesCache(SOM::Network network, ObjectPositions trackPositions)
|
||||
FeaturesClassifierCache::FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions)
|
||||
: _network {std::move(network)},
|
||||
_trackPositions {std::move(trackPositions)}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
} // namespace Similarity
|
||||
} // namespace Recommendation
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "som/Network.hpp"
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
class FeaturesClassifierCache
|
||||
{
|
||||
public:
|
||||
static void invalidate();
|
||||
|
||||
static std::optional<FeaturesClassifierCache> read();
|
||||
void write() const;
|
||||
|
||||
private:
|
||||
using ObjectPositions = std::unordered_map<Database::IdType, std::unordered_set<SOM::Position>>;
|
||||
|
||||
FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions);
|
||||
|
||||
static std::optional<SOM::Network> createNetworkFromCacheFile(const std::filesystem::path& path);
|
||||
static std::optional<ObjectPositions> createObjectPositionsFromCacheFile(const std::filesystem::path& path);
|
||||
static bool objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path);
|
||||
|
||||
friend class FeaturesClassifier;
|
||||
|
||||
SOM::Network _network;
|
||||
ObjectPositions _trackPositions;
|
||||
};
|
||||
|
||||
} // namespace Recommendation
|
||||
+3
-3
@@ -17,14 +17,14 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SimilarityFeaturesDefs.hpp"
|
||||
#include "FeaturesDefs.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Similarity {
|
||||
namespace Recommendation {
|
||||
|
||||
static const std::unordered_map<FeatureName, FeatureDef> featureDefinitions
|
||||
{
|
||||
@@ -398,5 +398,5 @@ getFeatureNames()
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Similarity
|
||||
} // namespace Recommendation
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Similarity {
|
||||
namespace Recommendation {
|
||||
|
||||
using FeatureName = std::string;
|
||||
using FeatureNames = std::unordered_set<FeatureName>;
|
||||
@@ -46,4 +46,4 @@ struct FeatureSettings
|
||||
};
|
||||
using FeatureSettingsMap = std::unordered_map<FeatureName, FeatureSettings>;
|
||||
|
||||
} // namespace Similarity
|
||||
} // namespace Recommendation
|
||||
+6
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,33 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "som/Network.hpp"
|
||||
|
||||
namespace Similarity {
|
||||
|
||||
class FeaturesCache
|
||||
namespace Recommendation
|
||||
{
|
||||
public:
|
||||
class IClassifier;
|
||||
|
||||
static void invalidate();
|
||||
std::unique_ptr<IClassifier> createClustersClassifier();
|
||||
}
|
||||
|
||||
static std::optional<FeaturesCache> read();
|
||||
void write();
|
||||
|
||||
private:
|
||||
using ObjectPositions = std::map<Database::IdType, std::set<SOM::Position>>;
|
||||
|
||||
FeaturesCache(SOM::Network network, ObjectPositions trackPositions);
|
||||
|
||||
friend class FeaturesSearcher;
|
||||
|
||||
SOM::Network _network;
|
||||
ObjectPositions _trackPositions;
|
||||
};
|
||||
|
||||
} // namespace Similarity
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 "recommendation/IClassifier.hpp"
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
std::unique_ptr<IClassifier> createFeaturesClassifier();
|
||||
}
|
||||
|
||||
@@ -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 <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
|
||||
class IClassifier
|
||||
{
|
||||
public:
|
||||
virtual ~IClassifier() = default;
|
||||
|
||||
virtual std::string_view getName() const = 0;
|
||||
|
||||
virtual bool init(Database::Session& session, bool databaseChanged) = 0;
|
||||
virtual void requestCancelInit() = 0;
|
||||
|
||||
virtual std::vector<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const = 0;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <functional>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IEngine
|
||||
{
|
||||
public:
|
||||
virtual ~IEngine() = default;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
virtual void requestReload() = 0;
|
||||
virtual Wt::Signal<>& reloaded() = 0;
|
||||
|
||||
// Closest results first
|
||||
virtual std::vector<Database::IdType> getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarTracks(Database::Session& session, const std::unordered_set<Database::IdType>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) = 0;
|
||||
virtual std::vector<Database::IdType> getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEngine> createEngine(Database::Db& db);
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
add_library(lmsscanner SHARED
|
||||
impl/AcousticBrainzUtils.cpp
|
||||
impl/MediaScanner.cpp
|
||||
impl/MediaScannerStats.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsscanner INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsscanner PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmsscanner PRIVATE
|
||||
lmsdatabase
|
||||
lmsmetadata
|
||||
lmsutils
|
||||
)
|
||||
|
||||
target_link_libraries(lmsscanner PUBLIC
|
||||
wt
|
||||
)
|
||||
|
||||
install(TARGETS lmsscanner DESTINATION lib)
|
||||
|
||||
+6
-5
@@ -25,9 +25,10 @@
|
||||
#include <Wt/WIOService.h>
|
||||
#include <Wt/Http/Client.h>
|
||||
|
||||
#include "utils/Config.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
|
||||
namespace AcousticBrainz
|
||||
@@ -39,7 +40,7 @@ getJsonData(const UUID& mbid)
|
||||
{
|
||||
static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/";
|
||||
|
||||
const std::string url {ServiceProvider<Config>::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"};
|
||||
const std::string url {ServiceProvider<IConfig>::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"};
|
||||
|
||||
boost::asio::io_service ioService;
|
||||
|
||||
@@ -50,7 +51,7 @@ getJsonData(const UUID& mbid)
|
||||
|
||||
if (!client.get(url))
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "Cannot perform a GET request to url '" << url << "'";
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot perform a GET request to url '" << url << "'";
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -59,13 +60,13 @@ getJsonData(const UUID& mbid)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "GET request to url '" << url << "' failed: " << ec.message();
|
||||
LMS_LOG(DBUPDATER, ERROR) << "GET request to url '" << url << "' failed: " << ec.message();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.status() != 200)
|
||||
{
|
||||
LMS_LOG(SIMILARITY, ERROR) << "GET request to url '" << url << "' failed: status = " << msg.status() << ", body = " << msg.body();
|
||||
LMS_LOG(DBUPDATER, ERROR) << "GET request to url '" << url << "' failed: status = " << msg.status() << ", body = " << msg.body();
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "utils/UUID.hpp"
|
||||
class UUID;
|
||||
|
||||
namespace AcousticBrainz
|
||||
{
|
||||
@@ -23,15 +23,18 @@
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
#include "AcousticBrainzUtils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
@@ -60,7 +63,7 @@ getNextFirstOfMonth(Wt::WDate current)
|
||||
}
|
||||
|
||||
bool
|
||||
isFileSupported(const std::filesystem::path& file, const std::set<std::filesystem::path>& extensions)
|
||||
isFileSupported(const std::filesystem::path& file, const std::unordered_set<std::filesystem::path>& extensions)
|
||||
{
|
||||
return (extensions.find(file.extension()) != extensions.end());
|
||||
}
|
||||
@@ -206,18 +209,27 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames)
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
std::unique_ptr<IMediaScanner>
|
||||
createMediaScanner(Database::Db& db)
|
||||
{
|
||||
return std::make_unique<MediaScanner>(db);
|
||||
}
|
||||
|
||||
MediaScanner::MediaScanner(Database::Db& db)
|
||||
: _dbSession {db}
|
||||
{
|
||||
// For now, always use TagLib
|
||||
_metadataParser = std::make_unique<MetaData::TagLibParser>();
|
||||
|
||||
_ioService.setThreadCount(1);
|
||||
|
||||
refreshScanSettings();
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::setAddon(MediaScannerAddon& addon)
|
||||
MediaScanner::~MediaScanner()
|
||||
{
|
||||
_addons.push_back(&addon);
|
||||
if (_running)
|
||||
stop();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -242,9 +254,6 @@ MediaScanner::stop(void)
|
||||
{
|
||||
_running = false;
|
||||
|
||||
for (auto& addon : _addons)
|
||||
addon->requestStop();
|
||||
|
||||
_scheduleTimer.cancel();
|
||||
|
||||
_ioService.stop();
|
||||
@@ -290,7 +299,7 @@ MediaScanner::scheduleNextScan()
|
||||
|
||||
refreshScanSettings();
|
||||
|
||||
Wt::WDateTime now {Wt::WLocalDateTime::currentServerDateTime().toUTC()};
|
||||
const Wt::WDateTime now {Wt::WLocalDateTime::currentServerDateTime().toUTC()};
|
||||
|
||||
Wt::WDate nextScanDate;
|
||||
switch (_updatePeriod)
|
||||
@@ -341,33 +350,21 @@ MediaScanner::scheduleNextScan()
|
||||
void
|
||||
MediaScanner::countAllFiles(ScanStats& stats)
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
stats.filesToScan = 0;
|
||||
|
||||
std::filesystem::recursive_directory_iterator itPath {_mediaDirectory, std::filesystem::directory_options::follow_directory_symlink, ec};
|
||||
if (ec)
|
||||
exploreFilesRecursive(_mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot iterate over '" << _mediaDirectory.string() << "': " << ec.message();
|
||||
return;
|
||||
}
|
||||
if (!_running)
|
||||
return false;
|
||||
|
||||
std::filesystem::recursive_directory_iterator itEnd;
|
||||
while (_running && itPath != itEnd)
|
||||
{
|
||||
const std::filesystem::path& path {*itPath};
|
||||
|
||||
if (!ec)
|
||||
if (!ec && isFileSupported(path, _fileExtensions))
|
||||
{
|
||||
if (std::filesystem::is_regular_file(path) && isFileSupported(path, _fileExtensions))
|
||||
stats.filesToScan ++;
|
||||
|
||||
if (stats.filesToScan % 250 == 0)
|
||||
notifyInProgressIfNeeded(stats);
|
||||
stats.filesToScan++;
|
||||
notifyInProgressIfNeeded(stats);
|
||||
}
|
||||
|
||||
itPath.increment(ec);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
@@ -428,13 +425,14 @@ MediaScanner::scan(boost::system::error_code err)
|
||||
if (_running)
|
||||
checkDuplicatedAudioFiles(stats);
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), duplicates = " << stats.duplicates.size();
|
||||
// Now update all the track features if needed
|
||||
fetchTrackFeatures(stats);
|
||||
|
||||
if (_running)
|
||||
{
|
||||
for (auto& addon : _addons)
|
||||
addon->preScanComplete();
|
||||
}
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size();
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimizing db...";
|
||||
_dbSession.optimize();
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimize db done!";
|
||||
|
||||
if (_running)
|
||||
{
|
||||
@@ -457,41 +455,104 @@ MediaScanner::scan(boost::system::error_code err)
|
||||
_curState = State::NotScheduled;
|
||||
_inProgressScanStats.reset();
|
||||
}
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimizing db...";
|
||||
_dbSession.optimize();
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimize db done!";
|
||||
bool
|
||||
MediaScanner::fetchTrackFeatures(Database::IdType trackId, const UUID& MBID)
|
||||
{
|
||||
std::map<std::string, double> features;
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Fetching low level features for track '" << MBID.getAsString() << "'";
|
||||
const std::string data {AcousticBrainz::extractLowLevelFeatures(MBID)};
|
||||
if (data.empty())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Track " << trackId << ", MBID = '" << MBID.getAsString() << "': cannot extract features using AcousticBrainz";
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
auto uniqueTransaction {_dbSession.createUniqueTransaction()};
|
||||
|
||||
Wt::Dbo::ptr<Database::Track> track {Database::Track::getById(_dbSession, trackId)};
|
||||
if (!track)
|
||||
return false;
|
||||
|
||||
Database::TrackFeatures::create(_dbSession, track, data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::fetchTrackFeatures(ScanStats& stats)
|
||||
{
|
||||
if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features)
|
||||
return;
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Fetching missing track features...";
|
||||
|
||||
struct TrackInfo
|
||||
{
|
||||
Database::IdType id;
|
||||
UUID mbid;
|
||||
};
|
||||
|
||||
const auto tracksToFetch {[&]()
|
||||
{
|
||||
std::vector<TrackInfo> res;
|
||||
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
|
||||
auto tracks {Database::Track::getAllWithMBIDAndMissingFeatures(_dbSession)};
|
||||
for (const auto& track : tracks)
|
||||
res.emplace_back(TrackInfo {track.id(), *track->getMBID()});
|
||||
|
||||
return res;
|
||||
}()};
|
||||
|
||||
stats.featuresToFetch = tracksToFetch.size();
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!";
|
||||
|
||||
for (const TrackInfo& trackToFetch : tracksToFetch)
|
||||
{
|
||||
if (!_running)
|
||||
return;
|
||||
|
||||
if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid))
|
||||
stats.featuresFetched++;
|
||||
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Track features fetched!";
|
||||
}
|
||||
|
||||
void
|
||||
MediaScanner::refreshScanSettings()
|
||||
{
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
|
||||
ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)};
|
||||
ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)};
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Using scan settings version " << scanSettings->getScanVersion();
|
||||
LMS_LOG(DBUPDATER, INFO) << "Using scan settings version " << scanSettings->getScanVersion();
|
||||
|
||||
_scanVersion = scanSettings->getScanVersion();
|
||||
_startTime = scanSettings->getUpdateStartTime();
|
||||
_updatePeriod = scanSettings->getUpdatePeriod();
|
||||
_scanVersion = scanSettings->getScanVersion();
|
||||
_startTime = scanSettings->getUpdateStartTime();
|
||||
_updatePeriod = scanSettings->getUpdatePeriod();
|
||||
|
||||
_fileExtensions = scanSettings->getAudioFileExtensions();
|
||||
_mediaDirectory = scanSettings->getMediaDirectory();
|
||||
_fileExtensions = scanSettings->getAudioFileExtensions();
|
||||
_mediaDirectory = scanSettings->getMediaDirectory();
|
||||
_recommendationEngineType = scanSettings->getRecommendationEngineType();
|
||||
|
||||
auto clusterTypes = scanSettings->getClusterTypes();
|
||||
std::set<std::string> clusterTypeNames;
|
||||
auto clusterTypes = scanSettings->getClusterTypes();
|
||||
std::set<std::string> clusterTypeNames;
|
||||
|
||||
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
||||
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
||||
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
|
||||
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
||||
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
||||
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
|
||||
|
||||
_metadataParser.setClusterTypeNames(clusterTypeNames);
|
||||
}
|
||||
_metadataParser->setClusterTypeNames(clusterTypeNames);
|
||||
|
||||
for (auto& addon : _addons)
|
||||
addon->refreshSettings();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -519,8 +580,6 @@ MediaScanner::notifyInProgressIfNeeded(const ScanStats& stats)
|
||||
void
|
||||
MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats)
|
||||
{
|
||||
notifyInProgressIfNeeded(stats);
|
||||
|
||||
Wt::WDateTime lastWriteTime;
|
||||
try
|
||||
{
|
||||
@@ -548,7 +607,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<MetaData::Track> trackInfo {_metadataParser.parse(file)};
|
||||
std::optional<MetaData::Track> trackInfo {_metadataParser->parse(file)};
|
||||
if (!trackInfo)
|
||||
{
|
||||
stats.errors.emplace_back(file, ScanErrorType::CannotParseFile);
|
||||
@@ -666,6 +725,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
track.modify()->setYear(*trackInfo->originalYear);
|
||||
|
||||
track.modify()->setMBID(trackInfo->musicBrainzRecordID);
|
||||
track.modify()->setFeatures({}); // TODO: only if MBID changed?
|
||||
track.modify()->setHasCover(trackInfo->hasCover);
|
||||
track.modify()->setCopyright(trackInfo->copyright);
|
||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||
@@ -674,41 +734,32 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
void
|
||||
MediaScanner::scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats)
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
std::filesystem::recursive_directory_iterator itPath {_mediaDirectory, std::filesystem::directory_options::follow_directory_symlink, ec};
|
||||
if (ec)
|
||||
exploreFilesRecursive(mediaDirectory, [&](std::error_code ec, const std::filesystem::path& path)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot iterate over '" << mediaDirectory.string() << "': " << ec.message();
|
||||
stats.errors.emplace_back(ScanError {mediaDirectory, ScanErrorType::CannotReadFile, ec.message()});
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::recursive_directory_iterator itEnd;
|
||||
while (_running && itPath != itEnd)
|
||||
{
|
||||
const std::filesystem::path& path {*itPath};
|
||||
if (!_running)
|
||||
return false;
|
||||
|
||||
if (ec)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
|
||||
stats.errors.emplace_back(ScanError {path, ScanErrorType::CannotReadFile, ec.message()});
|
||||
}
|
||||
else if (std::filesystem::is_regular_file(path))
|
||||
else if (isFileSupported(path, _fileExtensions))
|
||||
{
|
||||
if (isFileSupported(path, _fileExtensions))
|
||||
scanAudioFile(path, forceScan, stats );
|
||||
scanAudioFile(path, forceScan, stats );
|
||||
|
||||
notifyInProgressIfNeeded(stats);
|
||||
}
|
||||
|
||||
itPath.increment(ec);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
notifyInProgress(stats);
|
||||
}
|
||||
|
||||
// Check if a file exists and is still in a media directory
|
||||
static bool
|
||||
checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDirectory, const std::set<std::filesystem::path>& extensions)
|
||||
checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDirectory, const std::unordered_set<std::filesystem::path>& extensions)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -769,6 +820,8 @@ MediaScanner::removeMissingTracks(ScanStats& stats)
|
||||
stats.deletions++;
|
||||
}
|
||||
}
|
||||
|
||||
notifyInProgressIfNeeded(stats);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,56 +29,39 @@
|
||||
|
||||
#include <boost/asio/system_timer.hpp>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "scanner/IMediaScanner.hpp"
|
||||
|
||||
#include "MediaScannerAddon.hpp"
|
||||
#include "MediaScannerStats.hpp"
|
||||
class UUID;
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
class MediaScanner
|
||||
class MediaScanner : public IMediaScanner
|
||||
{
|
||||
public:
|
||||
MediaScanner(Database::Db& db);
|
||||
~MediaScanner();
|
||||
|
||||
void setAddon(MediaScannerAddon& addon);
|
||||
MediaScanner(const MediaScanner&) = delete;
|
||||
MediaScanner(MediaScanner&&) = delete;
|
||||
MediaScanner& operator=(const MediaScanner&) = delete;
|
||||
MediaScanner& operator=(MediaScanner&&) = delete;
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void restart();
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void restart() override;
|
||||
|
||||
// Async requests
|
||||
void requestImmediateScan();
|
||||
void requestReschedule();
|
||||
void requestImmediateScan() override;
|
||||
void requestReschedule() override ;
|
||||
|
||||
Status getStatus() override;
|
||||
|
||||
enum class State
|
||||
{
|
||||
NotScheduled,
|
||||
Scheduled,
|
||||
InProgress,
|
||||
};
|
||||
|
||||
struct Status
|
||||
{
|
||||
State currentState {State::NotScheduled};
|
||||
Wt::WDateTime nextScheduledScan;
|
||||
std::optional<ScanStats> lastCompleteScanStats;
|
||||
std::optional<ScanProgressStats> inProgressScanStats;
|
||||
};
|
||||
|
||||
Status getStatus();
|
||||
|
||||
// Called just after scan complete
|
||||
Wt::Signal<>& scanComplete() { return _sigScanComplete; }
|
||||
|
||||
// Called during scan in progress
|
||||
Wt::Signal<ScanProgressStats>& scanInProgress() { return _sigScanInProgress; }
|
||||
|
||||
// Called after a schedule
|
||||
Wt::Signal<Wt::WDateTime>& scheduled() { return _sigScheduled; }
|
||||
Wt::Signal<>& scanComplete() override { return _sigScanComplete; }
|
||||
Wt::Signal<ScanProgressStats>& scanInProgress() override { return _sigScanInProgress; }
|
||||
Wt::Signal<Wt::WDateTime>& scheduled() override { return _sigScheduled; }
|
||||
|
||||
private:
|
||||
|
||||
@@ -90,6 +73,8 @@ class MediaScanner
|
||||
void scan(boost::system::error_code ec);
|
||||
|
||||
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
||||
bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID);
|
||||
void fetchTrackFeatures(ScanStats& stats);
|
||||
|
||||
// Helpers
|
||||
void refreshScanSettings();
|
||||
@@ -103,7 +88,7 @@ class MediaScanner
|
||||
void notifyInProgressIfNeeded(const ScanStats& stats);
|
||||
void notifyInProgress(const ScanStats& stats);
|
||||
|
||||
bool _running {false};
|
||||
bool _running {};
|
||||
Wt::WIOService _ioService;
|
||||
boost::asio::system_timer _scheduleTimer {_ioService};
|
||||
Wt::Signal<> _sigScanComplete;
|
||||
@@ -111,8 +96,7 @@ class MediaScanner
|
||||
std::chrono::system_clock::time_point _lastScanInProgressEmit {};
|
||||
Wt::Signal<Wt::WDateTime> _sigScheduled;
|
||||
Database::Session _dbSession;
|
||||
MetaData::TagLibParser _metadataParser;
|
||||
std::vector<MediaScannerAddon*> _addons;
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
|
||||
std::mutex _statusMutex;
|
||||
State _curState {State::NotScheduled};
|
||||
@@ -121,11 +105,12 @@ class MediaScanner
|
||||
Wt::WDateTime _nextScheduledScan;
|
||||
|
||||
// Current scan settings
|
||||
std::size_t _scanVersion {};
|
||||
Wt::WTime _startTime;
|
||||
std::size_t _scanVersion {};
|
||||
Wt::WTime _startTime;
|
||||
Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
|
||||
std::set<std::filesystem::path> _fileExtensions;
|
||||
std::unordered_set<std::filesystem::path> _fileExtensions;
|
||||
std::filesystem::path _mediaDirectory;
|
||||
Database::ScanSettings::RecommendationEngineType _recommendationEngineType;
|
||||
|
||||
|
||||
}; // class MediaScanner
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MediaScannerStats.hpp"
|
||||
#include "scanner/MediaScannerStats.hpp"
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 Scanner {
|
||||
|
||||
class IMediaScanner
|
||||
{
|
||||
public:
|
||||
virtual ~IMediaScanner() = default;
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void restart() = 0;
|
||||
|
||||
// Async requests
|
||||
virtual void requestImmediateScan() = 0;
|
||||
virtual void requestReschedule() = 0;
|
||||
|
||||
|
||||
enum class State
|
||||
{
|
||||
NotScheduled,
|
||||
Scheduled,
|
||||
InProgress,
|
||||
};
|
||||
|
||||
struct Status
|
||||
{
|
||||
State currentState {State::NotScheduled};
|
||||
Wt::WDateTime nextScheduledScan;
|
||||
std::optional<ScanStats> lastCompleteScanStats;
|
||||
std::optional<ScanProgressStats> inProgressScanStats;
|
||||
};
|
||||
|
||||
virtual Status getStatus() = 0;
|
||||
|
||||
// Called just after scan complete
|
||||
virtual Wt::Signal<>& scanComplete() = 0;
|
||||
|
||||
// Called during scan in progress
|
||||
virtual Wt::Signal<ScanProgressStats>& scanInProgress() = 0;
|
||||
|
||||
// Called after a schedule
|
||||
virtual Wt::Signal<Wt::WDateTime>& scheduled() = 0;
|
||||
|
||||
};
|
||||
|
||||
std::unique_ptr<IMediaScanner> createMediaScanner(Database::Db& db);
|
||||
|
||||
|
||||
} // Scanner
|
||||
|
||||
+4
-1
@@ -77,10 +77,13 @@ namespace Scanner {
|
||||
std::size_t skips {}; // no change since last scan
|
||||
std::size_t scans {}; // actually scanned filed
|
||||
|
||||
std::size_t additions {}; // Added in DB
|
||||
std::size_t additions {}; // added in DB
|
||||
std::size_t deletions {}; // removed from DB
|
||||
std::size_t updates {}; // updated file in DB
|
||||
|
||||
std::size_t featuresFetched {}; // features fetched in DB
|
||||
std::size_t featuresToFetch {}; // features to be fetched in DB
|
||||
|
||||
std::vector<ScanError> errors;
|
||||
std::vector<ScanDuplicate> duplicates;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
add_library(lmssom STATIC
|
||||
impl/DataNormalizer.cpp
|
||||
impl/Network.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmssom INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmssom PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(lmssom PUBLIC
|
||||
lmsutils
|
||||
)
|
||||
|
||||
set_property(TARGET lmssom PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "DataNormalizer.hpp"
|
||||
#include "som/DataNormalizer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Network.hpp"
|
||||
#include "som/Network.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -201,9 +201,9 @@ Network::getClosestRefVectorPosition(const InputVector& data, InputVector::Dista
|
||||
}
|
||||
|
||||
std::optional<Position>
|
||||
Network::getClosestRefVectorPosition(const std::set<Position>& refVectorsPosition, InputVector::Distance maxDistance) const
|
||||
Network::getClosestRefVectorPosition(const std::unordered_set<Position>& refVectorsPosition, InputVector::Distance maxDistance) const
|
||||
{
|
||||
std::set<Position> neighboursPosition;
|
||||
std::unordered_set<Position> neighboursPosition;
|
||||
for (const Position& refVectorPosition : refVectorsPosition)
|
||||
{
|
||||
if (refVectorPosition.y > 0)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user