- ${filters class="Lms-explore-filters"}
- ${info class="Lms-explore-info hidden-md hidden-xs"}
+
+ ${contents class="Lms-explore-contents"}
-
- ${contents}
+
+ ${filters class="Lms-explore-filters"}
+ ${info class="Lms-explore-info"}
diff --git a/approot/messages.xml b/approot/messages.xml
index 3d056f79..51971675 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -76,11 +76,15 @@
Value
Various artists
+
+
Similar artists
+
Recently added
Top artists
+
Similar albums
Copyright
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 89ff1ce3..184a2a8f 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -70,17 +70,21 @@
Filtres
Albums
Rechercher...
-
Montrer plus
+
Voir plus
Pistes
Type
Valeur
Artistes divers
+
+
Artists similaires
+
Ajouts récents
Artistes populaires
+
Albums similaires
Copyright
diff --git a/approot/releaseinfo.xml b/approot/releaseinfo.xml
new file mode 100644
index 00000000..1b933078
--- /dev/null
+++ b/approot/releaseinfo.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ ${tr:Lms.Explore.ReleaseInfo.similar-releases}
+ ${similar-releases}
+
+
+
diff --git a/approot/releaselink.xml b/approot/releaselink.xml
new file mode 100644
index 00000000..8ccfdc7f
--- /dev/null
+++ b/approot/releaselink.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/approot/releasesinfo.xml b/approot/releasesinfo.xml
index 85c8f4c6..56bf978e 100644
--- a/approot/releasesinfo.xml
+++ b/approot/releasesinfo.xml
@@ -9,22 +9,4 @@
${recently-added}
-
-
-
-
diff --git a/conf/lms.conf b/conf/lms.conf
index 4c8acc54..75716c24 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -22,3 +22,7 @@ approot = "/usr/share/lms/approot";
# Turn on this option to allow the demo account creation/use
#demo = false;
+
+# Acoustic brainz's root API
+acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"
+
diff --git a/configure.ac b/configure.ac
index 4f4bee7f..9bf71e41 100644
--- a/configure.ac
+++ b/configure.ac
@@ -85,8 +85,8 @@ AC_CONFIG_FILES([Makefile
src/Makefile
test/Makefile
tools/Makefile
- tools/metadata/Makefile
- tools/clusterer/Makefile])
+ tools/feature-extractor/Makefile
+ tools/metadata/Makefile])
AC_OUTPUT
diff --git a/docroot/css/lms.css b/docroot/css/lms.css
index 0485a49b..95b0799e 100644
--- a/docroot/css/lms.css
+++ b/docroot/css/lms.css
@@ -18,6 +18,10 @@ a:hover {
justify-content: center;
}
+.Lms-contents {
+ margin-bottom: 78px;
+}
+
.Lms-show-more {
margin-top: 8px;
margin-bottom: 8px;
@@ -105,8 +109,8 @@ a:hover {
margin-bottom: 4px;
}
-.Lms-contents {
- margin-bottom: 78px;
+.Lms-explore-contents {
+ margin-bottom: 24px;
}
.Lms-explore-filters {
@@ -131,16 +135,6 @@ a:hover {
background-color: lightgrey;
}
-.Lms-explore-releasesinfo-entry {
- margin: 0px;
- padding-top: 4px;
- padding-bottom: 4px;
-}
-
-.Lms-explore-releasesinfo-entry:hover {
- background-color: lightgrey;
-}
-
.Lms-explore-release-cover {
max-width: 100%;
border-radius: 8px;
@@ -165,6 +159,17 @@ a:hover {
font-weight: bold;
}
+.Lms-explore-releaselink {
+ margin: 0px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.Lms-explore-releaselink:hover {
+ background-color: lightgrey;
+}
+
+
.Lms-explore-artists-entry {
min-height: 48px;
}
@@ -173,15 +178,6 @@ a:hover {
background-color: lightgrey;
}
-.Lms-explore-artistsinfo-entry {
- min-height: 28px;
- margin: 0px;
-}
-
-.Lms-explore-artistsinfo-entry:hover {
- background-color: lightgrey;
-}
-
.Lms-explore-artist-entry {
padding-top: 8px;
padding-bottom: 8px;
@@ -191,6 +187,16 @@ a:hover {
background-color: lightgrey;
}
+.Lms-explore-artistlink {
+ min-height: 28px;
+ margin: 0px;
+}
+
+.Lms-explore-artistlink:hover {
+ background-color: lightgrey;
+}
+
+
.Lms-explore-tracks-entry {
min-height: 32px;
padding-top: 2px;
diff --git a/src/Makefile.am b/src/Makefile.am
index 1efba30c..5e27ba28 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,23 +1,33 @@
bin_PROGRAMS = lms
lms_SOURCES = \
- $(srcdir)/main/main.cpp \
$(srcdir)/av/AvInfo.cpp \
$(srcdir)/av/AvTranscoder.cpp \
$(srcdir)/cover/CoverArtGrabber.cpp \
$(srcdir)/database/Artist.cpp \
$(srcdir)/database/Cluster.cpp \
$(srcdir)/database/DatabaseHandler.cpp \
- $(srcdir)/database/TrackList.cpp \
+ $(srcdir)/database/TrackFeature.cpp \
+ $(srcdir)/database/TrackList.cpp \
$(srcdir)/database/Release.cpp \
$(srcdir)/database/ScanSettings.cpp \
+ $(srcdir)/database/SimilaritySettings.cpp \
$(srcdir)/database/SqlQuery.cpp \
$(srcdir)/database/Track.cpp \
$(srcdir)/database/User.cpp \
$(srcdir)/image/Image.cpp \
+ $(srcdir)/main/main.cpp \
+ $(srcdir)/main/Services.cpp \
$(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/metadata/TagLibParser.cpp \
$(srcdir)/scanner/MediaScanner.cpp \
+ $(srcdir)/similarity/SimilaritySearcher.cpp \
+ $(srcdir)/similarity/cluster/SimilarityClusterSearcher.cpp \
+ $(srcdir)/similarity/som/AcousticBrainzUtils.cpp \
+ $(srcdir)/similarity/som/DataNormalizer.cpp \
+ $(srcdir)/similarity/som/Network.cpp \
+ $(srcdir)/similarity/som/SimilaritySOMScannerAddon.cpp \
+ $(srcdir)/similarity/som/SimilaritySOMSearcher.cpp \
$(srcdir)/ui/Auth.cpp \
$(srcdir)/ui/LmsApplication.cpp \
$(srcdir)/ui/LmsApplicationGroup.cpp \
@@ -30,11 +40,15 @@ lms_SOURCES = \
$(srcdir)/ui/admin/UserView.cpp \
$(srcdir)/ui/admin/UsersView.cpp \
$(srcdir)/ui/common/Validators.cpp \
- $(srcdir)/ui/explore/ArtistView.cpp \
+ $(srcdir)/ui/explore/ArtistInfoView.cpp \
+ $(srcdir)/ui/explore/ArtistLink.cpp \
$(srcdir)/ui/explore/ArtistsInfoView.cpp \
+ $(srcdir)/ui/explore/ArtistView.cpp \
$(srcdir)/ui/explore/ArtistsView.cpp \
$(srcdir)/ui/explore/Explore.cpp \
$(srcdir)/ui/explore/Filters.cpp \
+ $(srcdir)/ui/explore/ReleaseInfoView.cpp \
+ $(srcdir)/ui/explore/ReleaseLink.cpp \
$(srcdir)/ui/explore/ReleasesInfoView.cpp \
$(srcdir)/ui/explore/ReleasesView.cpp \
$(srcdir)/ui/explore/ReleaseView.cpp \
diff --git a/src/clusterer/Clusterer.hpp b/src/clusterer/Clusterer.hpp
deleted file mode 100644
index 809fd79e..00000000
--- a/src/clusterer/Clusterer.hpp
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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
.
- */
-
-#pragma once
-
-#include
-#include "SOM.hpp"
-#include "DataNormalizer.hpp"
-
-/*
- * For each InputVector, associate vector values
- */
-template
-class Clusterer
-{
- public:
- using SampleType = std::pair;
- using Cluster = std::vector;
-
- Clusterer(const std::vector& samples, std::size_t inputDimCount, std::size_t iterationCount);
-
- const Cluster& getCluster(const SOM::InputVector& data) const;
-
- // Sorted results (best first)
- std::vector getClusters(const SOM::InputVector& data, std::size_t nbClusters) const;
-
- const std::vector& getAllClusters() const;
-
- void dump(std::ostream& os) const;
-
- private:
-
- void train(const std::vector>& samples, std::size_t iterationCount);
-
- std::vector& getValues(SOM::Coords coords);
- const std::vector& getValues(SOM::Coords coords) const;
-
- std::size_t _width;
- std::size_t _height;
- std::vector> _values; // Map of T vectors
- SOM::DataNormalizer _dataNormalizer;
- SOM::Network _network;
-};
-
-
-template
-Clusterer::Clusterer(const std::vector& samples, std::size_t inputDimCount, std::size_t iterationCount)
-:
-_width(std::sqrt(samples.size()/20)),
-_height(std::sqrt(samples.size()/20)),
-_dataNormalizer(inputDimCount),
-_network(_width, _height, inputDimCount)
-{
- _values.resize(_width * _height);
- train(samples, iterationCount);
-}
-
-template
-std::vector&
-Clusterer::getValues(SOM::Coords coords)
-{
- return _values[ coords.x + coords.y*_width ];
-}
-
-template
-const std::vector&
-Clusterer::getValues(SOM::Coords coords) const
-{
- return _values[ coords.x + coords.y*_width ];
-}
-
-
-template
-void
-Clusterer::train(const std::vector>& samples, std::size_t iterationCount)
-{
- // Train
- {
- std::vector inputVectors;
- inputVectors.reserve(samples.size());
-
- for (const auto& sample : samples)
- {
- inputVectors.push_back(sample.first);
- }
-
- _dataNormalizer.computeNormalizationFactors(inputVectors);
-
- for (auto& inputVector : inputVectors)
- _dataNormalizer.normalizeData(inputVector);
-
- _network.train(inputVectors, iterationCount);
- }
-
- // Classify data
- for (const auto& sample : samples)
- {
- auto inputVector = sample.first;
- const auto& value = sample.second;
-
- _dataNormalizer.normalizeData(inputVector);
- auto coords = _network.classify(inputVector);
- auto& values = getValues(coords);
-
- values.push_back(value);
- }
-}
-
-template
-const typename Clusterer::Cluster&
-Clusterer::getCluster(const SOM::InputVector& inputVector) const
-{
- auto inputVectorNormalized = inputVector;
- _dataNormalizer.normalizeData(inputVectorNormalized);
-
- return getValues(_network.classify(inputVectorNormalized));
-}
-
-template
-std::vector::Cluster>
-Clusterer::getClusters(const SOM::InputVector& inputVector, std::size_t nbClusters) const
-{
- auto inputVectorNormalized = inputVector;
- _dataNormalizer.normalizeData(inputVectorNormalized);
-
- std::vector::Cluster> res;
- for (auto& cluster : _network.classify(inputVectorNormalized, nbClusters))
- {
- res.push_back(getValues(cluster));
- }
-
- return res;
-}
-
-template
-const std::vector::Cluster>&
-Clusterer::getAllClusters() const
-{
- return _values;
-}
-
-template
-void
-Clusterer::dump(std::ostream& os) const
-{
- os << "Normalizer:" << std::endl;
- _dataNormalizer.dump(os);
- os << std::endl;
- os << "Internal network:" << std::endl;
- _network.dump(os);
- os << "Values: " << std::endl;
- for (std::size_t y = 0; y < _height; ++y)
- {
- for (std::size_t x = 0; x < _width; ++x)
- {
- os << "[";
- for (const auto& value : getValues({x, y}))
- os << value << " ";
- os << "] ";
- }
- os << std::endl;
- }
-
-}
-
diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp
index 4936a406..5ab9a7d1 100644
--- a/src/cover/CoverArtGrabber.cpp
+++ b/src/cover/CoverArtGrabber.cpp
@@ -50,15 +50,13 @@ namespace CoverArt {
Grabber::Grabber()
{
- if (!_defaultCover.load( Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"))
- throw LmsException("Cannot read default cover file");
}
-Grabber&
-Grabber::instance()
+void
+Grabber::setDefaultCover(boost::filesystem::path p)
{
- static Grabber instance;
- return instance;
+ if (!_defaultCover.load(p))
+ throw LmsException("Cannot read default cover file '" + p.string() + "'");
}
Image::Image
diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp
index e8d0b6a7..9ae412ab 100644
--- a/src/cover/CoverArtGrabber.hpp
+++ b/src/cover/CoverArtGrabber.hpp
@@ -32,18 +32,19 @@ namespace CoverArt {
class Grabber
{
public:
+ Grabber();
Grabber(const Grabber&) = delete;
Grabber& operator=(const Grabber&) = delete;
+ Grabber(Grabber&&) = delete;
+ Grabber& operator=(Grabber&&) = delete;
- static Grabber& instance();
+ void setDefaultCover(boost::filesystem::path defaultCoverPath);
std::vector getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, Image::Format format, std::size_t size);
std::vector getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, Image::Format format, std::size_t size);
private:
- Grabber();
-
Image::Image getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t size);
Image::Image getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t size);
diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp
index ccff0b47..ae479d12 100644
--- a/src/database/Artist.cpp
+++ b/src/database/Artist.cpp
@@ -199,6 +199,12 @@ Artist::getReleases(const std::set& clusterIds) const
return std::vector< Wt::Dbo::ptr > (res.begin(), res.end());
}
+std::vector>
+Artist::getTracks() const
+{
+ return std::vector>(_tracks.begin(), _tracks.end());
+}
+
std::vector>>
Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const
{
diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp
index 2ae1acda..9c8a0120 100644
--- a/src/database/Artist.hpp
+++ b/src/database/Artist.hpp
@@ -65,6 +65,7 @@ class Artist : public Wt::Dbo::Dbo
// Get the releases that have at least one track for this artist that belongs to optional cluster filters
std::vector> getReleases(const std::set& clusterIds = std::set()) const;
+ std::vector> getTracks() const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
diff --git a/src/database/Cluster.hpp b/src/database/Cluster.hpp
index 04b96279..104d6d24 100644
--- a/src/database/Cluster.hpp
+++ b/src/database/Cluster.hpp
@@ -37,7 +37,7 @@ class ScanSettings;
class Cluster : public Wt::Dbo::Dbo
{
public:
- typedef Wt::Dbo::ptr pointer;
+ using pointer = Wt::Dbo::ptr;
Cluster();
Cluster(Wt::Dbo::ptr type, std::string name);
@@ -50,7 +50,7 @@ class Cluster : public Wt::Dbo::Dbo
static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr type, std::string name);
// Accessors
- const std::string& getName(void) const { return _name; }
+ const std::string& getName() const { return _name; }
Wt::Dbo::ptr getType() const { return _clusterType; }
std::size_t getCount() const { return _tracks.size(); }
std::vector> getTracks(int offset, int limit) const;
diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp
index 5ec865db..8be91b9b 100644
--- a/src/database/DatabaseHandler.cpp
+++ b/src/database/DatabaseHandler.cpp
@@ -35,10 +35,12 @@
#include "Artist.hpp"
#include "Cluster.hpp"
-#include "TrackList.hpp"
#include "Release.hpp"
#include "ScanSettings.hpp"
+#include "SimilaritySettings.hpp"
#include "Track.hpp"
+#include "TrackList.hpp"
+#include "TrackFeature.hpp"
namespace Database {
@@ -104,8 +106,11 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
_session.mapClass("tracklist_entry");
_session.mapClass("release");
_session.mapClass