diff --git a/Makefile.am b/Makefile.am index 4c5d337c..954821e3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -28,6 +28,8 @@ lms_approot_DATA = \ approot/admin-users.xml \ approot/admin-initwizard.xml \ approot/artist.xml \ + approot/artistinfo.xml \ + approot/artistlink.xml \ approot/artists.xml \ approot/artistsinfo.xml \ approot/explore.xml \ @@ -38,6 +40,8 @@ lms_approot_DATA = \ approot/playhistory.xml \ approot/playqueue.xml \ approot/release.xml \ + approot/releaseinfo.xml \ + approot/releaselink.xml \ approot/releases.xml \ approot/releasesinfo.xml \ approot/settings.xml \ diff --git a/approot/artistinfo.xml b/approot/artistinfo.xml new file mode 100644 index 00000000..4e442180 --- /dev/null +++ b/approot/artistinfo.xml @@ -0,0 +1,10 @@ + + + + + +

${tr:Lms.Explore.ArtistInfo.similar-artists}

+ ${similar-artists} +
+ +
diff --git a/approot/artistlink.xml b/approot/artistlink.xml new file mode 100644 index 00000000..1b1959b9 --- /dev/null +++ b/approot/artistlink.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/approot/artistsinfo.xml b/approot/artistsinfo.xml index 9810bd8f..5a6e33dd 100644 --- a/approot/artistsinfo.xml +++ b/approot/artistsinfo.xml @@ -9,12 +9,4 @@ ${recently-added} - -
-
- ${name} -
-
-
- diff --git a/approot/explore.xml b/approot/explore.xml index 3ba8a202..047d14c5 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -3,12 +3,12 @@
-
- ${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} - -
-
-
-
- ${cover class="media-object"} -
-
-
- ${release-name} -
- ${}${artist-name}${} -
-
-
-
-
- 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("track"); + _session.mapClass("track_feature"); + _session.mapClass("track_feature_type"); _session.mapClass("scan_settings"); + _session.mapClass("similarity_settings"); _session.mapClass("auth_info"); _session.mapClass("auth_identity"); @@ -129,13 +134,18 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool) // Indexes _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_idx ON track(artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name ON tracklist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_feature_type_name_idx ON track_feature_type(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_feature_type_idx ON track_feature(type_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_feature_track_idx ON track_feature(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_feature_type_track_idx ON track_feature(type_id, track_id)"); } _users = new UserDatabase(_session); diff --git a/src/database/ScanSettings.cpp b/src/database/ScanSettings.cpp index 19e251f2..e5181187 100644 --- a/src/database/ScanSettings.cpp +++ b/src/database/ScanSettings.cpp @@ -45,7 +45,7 @@ namespace Database { ScanSettings::pointer ScanSettings::get(Wt::Dbo::Session& session) { - ScanSettings::pointer settings = session.find(); + pointer settings = session.find(); if (!settings) { settings = session.add(std::make_unique()); @@ -81,16 +81,17 @@ ScanSettings::setClusterTypes(const std::set& clusterTypeNames) assert(session()); // Create any missing cluster type - for (auto clusterTypeName : clusterTypeNames) + for (const auto& clusterTypeName : clusterTypeNames) { auto clusterType = ClusterType::getByName(*session(), clusterTypeName); if (!clusterType) { LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName; clusterType = ClusterType::create(*session(), clusterTypeName); + _clusterTypes.insert(clusterType); + needRescan = true; } - _clusterTypes.insert(clusterType); } // Delete no longer existing cluster types @@ -101,7 +102,6 @@ ScanSettings::setClusterTypes(const std::set& clusterTypeNames) { LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName(); clusterType.remove(); - needRescan = true; } } @@ -109,5 +109,6 @@ ScanSettings::setClusterTypes(const std::set& clusterTypeNames) _scanVersion += 1; } + } // namespace Database diff --git a/src/database/ScanSettings.hpp b/src/database/ScanSettings.hpp index a8bc39c2..2621ed1a 100644 --- a/src/database/ScanSettings.hpp +++ b/src/database/ScanSettings.hpp @@ -27,6 +27,7 @@ namespace Database { class ClusterType; + class ScanSettings : public Wt::Dbo::Dbo { public: @@ -39,8 +40,6 @@ class ScanSettings : public Wt::Dbo::Dbo Monthly }; - ScanSettings() {} - static pointer get(Wt::Dbo::Session& session); // Getters diff --git a/src/database/SimilaritySettings.cpp b/src/database/SimilaritySettings.cpp new file mode 100644 index 00000000..923c96d1 --- /dev/null +++ b/src/database/SimilaritySettings.cpp @@ -0,0 +1,130 @@ +/* + * 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 . + */ + +#include "SimilaritySettings.hpp" + +#include "utils/Logger.hpp" +#include "utils/Utils.hpp" + +#include "TrackFeature.hpp" + +namespace { + +std::set defaultTrackFeaturesNames = +{ + "lowlevel.average_loudness", + "lowlevel.barkbands_flatness_db.mean", + "lowlevel.dissonance.mean", + "lowlevel.dynamic_complexity", + "lowlevel.hfc.mean", // GOOD + "lowlevel.melbands_crest.mean", + "lowlevel.melbands_kurtosis.mean", + "lowlevel.melbands_skewness.mean", + "lowlevel.melbands_spread.mean", + "lowlevel.pitch_salience.mean", + "lowlevel.pitch_salience.var", + "lowlevel.silence_rate_30dB.mean", + "lowlevel.silence_rate_60dB.mean", + "lowlevel.spectral_centroid.mean", + "lowlevel.spectral_complexity.mean", + "lowlevel.spectral_decrease.mean", + "lowlevel.spectral_energy.mean", + "lowlevel.spectral_energyband_high.mean", + "lowlevel.spectral_energyband_low.mean", + "lowlevel.spectral_energyband_middle_high.mean", + "lowlevel.spectral_energyband_middle_low.mean", + "lowlevel.spectral_entropy.mean", + "lowlevel.spectral_flux.mean", + "lowlevel.spectral_kurtosis.mean", + "lowlevel.spectral_rms.mean", + "lowlevel.spectral_skewness.mean", + "lowlevel.spectral_spread.mean", + "lowlevel.spectral_strongpeak.mean", + "lowlevel.zerocrossingrate.mean", + "rhythm.beats_loudness.mean", // BAD + "rhythm.bpm", + "tonal.chords_changes_rate", // OK + "tonal.chords_number_rate", // BAD + "tonal.chords_strength.mean", // OK + "tonal.hpcp_entropy.mean", // GOOD +}; + +} // namespace + +namespace Database { + + +SimilaritySettings::pointer +SimilaritySettings::get(Wt::Dbo::Session& session) +{ + pointer settings = session.find(); + if (!settings) + { + settings = session.add(std::make_unique()); + settings.modify()->setTrackFeatureTypes(defaultTrackFeaturesNames); + } + + return settings; +} + +std::vector> +SimilaritySettings::getTrackFeatureTypes() const +{ + return std::vector>(_trackFeatureTypes.begin(), _trackFeatureTypes.end()); +} + +void +SimilaritySettings::setTrackFeatureTypes(const std::set& featuresNames) +{ + bool needRescan = false; + assert(session()); + + // Create any missing feature type + for (const auto& featureName : featuresNames) + { + auto featureType = TrackFeatureType::getByName(*session(), featureName); + if (!featureType) + { + LMS_LOG(DB, INFO) << "Creating feature type " << featureName; + featureType = TrackFeatureType::create(*session(), featureName); + _trackFeatureTypes.insert(featureType); + + needRescan = true; + } + } + + // Delete no longer existing feature type + for (auto trackFeatureType : _trackFeatureTypes) + { + if (std::none_of(featuresNames.begin(), featuresNames.end(), + [trackFeatureType](const std::string& name) { return name == trackFeatureType->getName(); })) + { + LMS_LOG(DB, INFO) << "Deleting track feature type " << trackFeatureType->getName(); + trackFeatureType.remove(); + } + } + + if (needRescan) + _scanVersion += 1; +} + + + +} // namespace Database + diff --git a/src/database/SimilaritySettings.hpp b/src/database/SimilaritySettings.hpp new file mode 100644 index 00000000..e8e855bd --- /dev/null +++ b/src/database/SimilaritySettings.hpp @@ -0,0 +1,66 @@ +/* + * 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 + +namespace Database { + +class TrackFeature; +class TrackFeatureType; + +class SimilaritySettings : public Wt::Dbo::Dbo +{ + public: + using pointer = Wt::Dbo::ptr; + + static pointer get(Wt::Dbo::Session& session); + + std::size_t getVersion() const { return _scanVersion; } + + std::vector> getTrackFeatureTypes() const; + void setTrackFeatureTypes(const std::set& featureTypeNames); + + void setNetworkData(std::string data) { _refFeaturesData = data; } + const std::string& getNetworkData() const { return _refFeaturesData; } + + void setNormalizationData(std::string data) { _normalizationData = data; } + const std::string& getNormalizationData() const { return _normalizationData; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scanVersion, "settings_version"); + Wt::Dbo::field(a, _normalizationData, "normalization_data"); + Wt::Dbo::field(a, _refFeaturesData, "ref_features_data"); + Wt::Dbo::hasMany(a, _trackFeatureTypes, Wt::Dbo::ManyToOne, "similarity_settings"); + } + + private: + + int _scanVersion = 0; + std::string _normalizationData; + std::string _refFeaturesData; + Wt::Dbo::collection> _trackFeatureTypes; +}; + + +} // namespace Database + diff --git a/src/database/Track.cpp b/src/database/Track.cpp index aa95619e..2fa2e508 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -26,6 +26,7 @@ #include "Artist.hpp" #include "Cluster.hpp" #include "Release.hpp" +#include "TrackFeature.hpp" #include "SqlQuery.hpp" namespace Database { @@ -107,6 +108,25 @@ Track::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit) return std::vector(res.begin(), res.end()); } +std::vector +Track::getAllWithMBIDAndMissingFeatures(Wt::Dbo::Session& session) +{ + Wt::Dbo::collection res = session.query + ("SELECT t FROM track t") + .where("LENGTH(t.mbid) > 0") + .where("NOT EXISTS (SELECT * FROM track_feature t_f WHERE t_f.track_id = t.id)"); + return std::vector(res.begin(), res.end()); +} + +std::vector +Track::getAllWithFeatures(Wt::Dbo::Session& session) +{ + Wt::Dbo::collection res = session.query + ("SELECT t FROM track t") + .where("EXISTS (SELECT * from track_feature t_f WHERE t_f.track_id = t.id)"); + return std::vector(res.begin(), res.end()); +} + std::vector Track::getClusters(void) const { @@ -115,6 +135,12 @@ Track::getClusters(void) const return clusters; } +bool +Track::hasTrackFeatures() const +{ + return !_trackFeatures.empty(); +} + static Wt::Dbo::Query< Track::pointer > getQuery(Wt::Dbo::Session& session, @@ -236,6 +262,18 @@ Track::getCopyrightURL() const return _copyrightURL != "" ? boost::make_optional(_copyrightURL) : boost::none; } +Wt::Dbo::ptr +Track::getTrackFeature(Wt::Dbo::ptr type) const +{ + assert(self()); + assert(IdIsValid(self()->id())); + assert(session()); + + return session()->find() + .where("type_id = ?").bind(type.id()) + .where("track_id = ?").bind(self()->id()); +} + std::vector> Track::getClusterGroups(std::vector clusterTypes, std::size_t size) const { diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 18c96618..06d6d587 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -36,15 +36,17 @@ namespace Database { class Artist; class Cluster; class ClusterType; -class TrackListEntry; class Release; +class TrackFeature; +class TrackFeatureType; +class TrackListEntry; class TrackStats; class Track : public Wt::Dbo::Dbo { public: - typedef Wt::Dbo::ptr pointer; + using pointer = Wt::Dbo::ptr; Track() {} Track(const boost::filesystem::path& p); @@ -68,6 +70,8 @@ class Track : public Wt::Dbo::Dbo static std::vector getMBIDDuplicates(Wt::Dbo::Session& session); static std::vector getChecksumDuplicates(Wt::Dbo::Session& session); static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); + static std::vector getAllWithMBIDAndMissingFeatures(Wt::Dbo::Session& session); // nested transaction + static std::vector getAllWithFeatures(Wt::Dbo::Session& session); // nested transaction // Create utility static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p); @@ -76,7 +80,8 @@ class Track : public Wt::Dbo::Dbo static void removeClusters(std::string type); // Accessors - void setScanVersion(std::size_t version) {_scanVersion = version; } + void setScanVersion(std::size_t version) { _scanVersion = version; } + void setSimilarityScanVersion(std::size_t version) { _similarityScanVersion = version; } void setTrackNumber(int num) { _trackNumber = num; } void setTotalTrackNumber(int num) { _totalTrackNumber = num; } void setDiscNumber(int num) { _discNumber = num; } @@ -96,8 +101,10 @@ class Track : public Wt::Dbo::Dbo void setArtist(Wt::Dbo::ptr artist) { _artist = artist; } void setRelease(Wt::Dbo::ptr release) { _release = release; } void eraseClusters() { _clusters.clear(); } + void eraseFeatures() { _trackFeatures.clear(); } std::size_t getScanVersion() const { return _scanVersion; } + std::size_t getSimilarityScanVersion() const { return _similarityScanVersion; } boost::optional getTrackNumber() const; boost::optional getTotalTrackNumber() const; boost::optional getDiscNumber() const; @@ -117,6 +124,9 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::ptr getArtist() const { return _artist; } Wt::Dbo::ptr getRelease() const { return _release; } std::vector> getClusters() const; + std::vector> getTrackFeatures() const; // ordered by feature's name + bool hasTrackFeatures() const; + Wt::Dbo::ptr getTrackFeature(Wt::Dbo::ptr type) const; std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const; @@ -124,6 +134,7 @@ class Track : public Wt::Dbo::Dbo void persist(Action& a) { Wt::Dbo::field(a, _scanVersion, "scan_version"); + Wt::Dbo::field(a, _similarityScanVersion, "similarity_version"); Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _totalTrackNumber, "total_track_number"); Wt::Dbo::field(a, _discNumber, "disc_number"); @@ -145,6 +156,7 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track"); + Wt::Dbo::hasMany(a, _trackFeatures, Wt::Dbo::ManyToOne, "track"); } private: @@ -154,6 +166,7 @@ class Track : public Wt::Dbo::Dbo static const std::size_t _maxCopyrightURLLength = 128; int _scanVersion = 0; + int _similarityScanVersion = 0; int _trackNumber = 0; int _totalTrackNumber = 0; int _discNumber = 0; @@ -178,6 +191,7 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::ptr _release; Wt::Dbo::collection> _clusters; Wt::Dbo::collection> _playlistEntries; + Wt::Dbo::collection> _trackFeatures; }; diff --git a/src/database/TrackFeature.cpp b/src/database/TrackFeature.cpp new file mode 100644 index 00000000..90671241 --- /dev/null +++ b/src/database/TrackFeature.cpp @@ -0,0 +1,58 @@ +/* + * 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 . + */ + +#include "TrackFeature.hpp" + +#include "SimilaritySettings.hpp" +#include "Track.hpp" + +namespace Database { + +TrackFeatureType::TrackFeatureType(std::string name) + : _name(name) +{ +} + +TrackFeatureType::pointer +TrackFeatureType::getByName(Wt::Dbo::Session& session, std::string name) +{ + return session.find().where("name = ?").bind(name); +} + +TrackFeatureType::pointer +TrackFeatureType::create(Wt::Dbo::Session& session, std::string name) +{ + return session.add(std::make_unique(name)); +} + + +TrackFeature::TrackFeature(Wt::Dbo::ptr type, Wt::Dbo::ptr track, double value) +: _type(type), +_track(track), +_value(value) +{ +} + +TrackFeature::pointer +TrackFeature::create(Wt::Dbo::Session& session, Wt::Dbo::ptr type, Wt::Dbo::ptr track, double value) +{ + return session.add(std::make_unique(type, track, value)); +} + +} // namespace Database diff --git a/src/database/TrackFeature.hpp b/src/database/TrackFeature.hpp new file mode 100644 index 00000000..0ce01a47 --- /dev/null +++ b/src/database/TrackFeature.hpp @@ -0,0 +1,97 @@ +/* + * 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 + +#include "Types.hpp" + +namespace Database { + +class Track; +class SimilaritySettings; + +class TrackFeatureType : public Wt::Dbo::Dbo +{ + public: + using pointer = Wt::Dbo::ptr; + + TrackFeatureType() = default; + TrackFeatureType(std::string name); + + // Find utility + static pointer getByName(Wt::Dbo::Session& session, std::string name); + + // Create utility + static pointer create(Wt::Dbo::Session& session, std::string name); + + // Accessors + const std::string& getName() const { return _name; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::belongsTo(a, _similaritySettings, "similarity_settings", Wt::Dbo::OnDeleteCascade); + } + + private: + + std::string _name; + + Wt::Dbo::ptr _similaritySettings; +}; + +class TrackFeature : public Wt::Dbo::Dbo +{ + public: + + using pointer = Wt::Dbo::ptr; + + TrackFeature() = default; + TrackFeature(Wt::Dbo::ptr type, Wt::Dbo::ptr track, double value); + + // Create utility + static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr type, Wt::Dbo::ptr track, double value); + + Wt::Dbo::ptr getType() const { return _type; } + double getValue() const { return _value; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _value, "value"); + Wt::Dbo::belongsTo(a, _type, "type", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + } + + private: + + Wt::Dbo::ptr _type; + Wt::Dbo::ptr _track; + double _value = 0.; +}; + + +} // namespace database + + diff --git a/src/main/Services.cpp b/src/main/Services.cpp new file mode 100644 index 00000000..c74b3109 --- /dev/null +++ b/src/main/Services.cpp @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#include "Services.hpp" + +#include "cover/CoverArtGrabber.hpp" +#include "scanner/MediaScanner.hpp" +#include "similarity/SimilaritySearcher.hpp" + +Services& getServices() +{ + static Services services; + return services; +} + diff --git a/src/main/Services.hpp b/src/main/Services.hpp new file mode 100644 index 00000000..a70fb35b --- /dev/null +++ b/src/main/Services.hpp @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +#include + +namespace CoverArt +{ + class Grabber; +} + +namespace Scanner { + class MediaScanner; +} + +namespace Similarity { + class Searcher; +} + + +struct Services +{ + std::unique_ptr coverArtGrabber; + std::unique_ptr mediaScanner; + std::unique_ptr similaritySearcher; +}; + +Services& getServices(); + diff --git a/src/main/main.cpp b/src/main/main.cpp index f047267f..8a4a4265 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -23,15 +23,17 @@ #include #include -#include "utils/Config.hpp" -#include "utils/Logger.hpp" #include "av/AvInfo.hpp" #include "av/AvTranscoder.hpp" +#include "cover/CoverArtGrabber.hpp" #include "image/Image.hpp" - #include "scanner/MediaScanner.hpp" - +#include "similarity/som/SimilaritySOMScannerAddon.hpp" +#include "similarity/SimilaritySearcher.hpp" #include "ui/LmsApplication.hpp" +#include "utils/Config.hpp" +#include "utils/Logger.hpp" +#include "Services.hpp" std::vector generateWtConfig(std::string execPath) { @@ -123,16 +125,24 @@ int main(int argc, char* argv[]) auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"); UserInterface::LmsApplicationGroupContainer appGroups; - Scanner::MediaScanner scanner(*connectionPool); + + // Service initialization order is important + getServices().mediaScanner = std::make_unique(*connectionPool); + + Similarity::SOMScannerAddon similaritySOMScannerAddon(*connectionPool); + + getServices().mediaScanner->setAddon(similaritySOMScannerAddon); + getServices().coverArtGrabber = std::make_unique(); + getServices().similaritySearcher = std::make_unique(similaritySOMScannerAddon); // bind entry point server.addEntryPoint(Wt::EntryPointType::Application, std::bind(UserInterface::LmsApplication::create, - std::placeholders::_1, std::ref(*connectionPool), std::ref(appGroups), std::ref(scanner))); + std::placeholders::_1, std::ref(*connectionPool), std::ref(appGroups))); // Start - LMS_LOG(MAIN, INFO) << "Starting Media scanner..."; - scanner.start(); + LMS_LOG(MAIN, INFO) << "Starting media scanner..."; + getServices().mediaScanner->start(); LMS_LOG(MAIN, INFO) << "Starting server..."; server.start(); @@ -145,8 +155,8 @@ int main(int argc, char* argv[]) LMS_LOG(MAIN, INFO) << "Stopping server..."; server.stop(); - LMS_LOG(MAIN, INFO) << "Stopping database updater..."; - scanner.stop(); + LMS_LOG(MAIN, INFO) << "Stopping media scanner..."; + getServices().mediaScanner->stop(); LMS_LOG(MAIN, INFO) << "Clean stop!"; res = EXIT_SUCCESS; diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp index a6b0ec59..7a8e03f7 100644 --- a/src/scanner/MediaScanner.cpp +++ b/src/scanner/MediaScanner.cpp @@ -27,13 +27,11 @@ #include #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 "utils/Logger.hpp" #include "utils/Path.hpp" #include "utils/Utils.hpp" @@ -198,6 +196,12 @@ _db(connectionPool) refreshScanSettings(); } +void +MediaScanner::setAddon(MediaScannerAddon& addon) +{ + _addons.push_back(&addon); +} + void MediaScanner::restart(void) { @@ -332,9 +336,11 @@ MediaScanner::scan(boost::system::error_code err) 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.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")"; - // Save the last scan only if it has been completed if (_running) { + for (auto& addon : _addons) + addon->preScanComplete(); + scheduleScan(); scanComplete().emit(stats); @@ -365,6 +371,11 @@ MediaScanner::refreshScanSettings() [](ClusterType::pointer clusterType) -> std::string { return clusterType->getName(); }); _metadataParser.setClusterTypeNames(clusterTypeNames); + + transaction.commit(); + + for (auto& addon : _addons) + addon->refreshSettings(); } void @@ -492,12 +503,14 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, // If file already exist, update data // Otherwise, create it + bool trackAdded = false; if (!track) { // Create a new song track = Track::create(_db.getSession(), file); LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'"; stats.additions++; + trackAdded = true; } else { @@ -583,6 +596,14 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, } transaction.commit(); + + for (auto& addon : _addons) + { + if (trackAdded) + addon->trackAdded(track.id()); + else + addon->trackUpdated(track.id()); + } } void @@ -753,5 +774,4 @@ MediaScanner::checkDuplicatedAudioFiles(Stats& stats) LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files done!"; } - } // namespace Scanner diff --git a/src/scanner/MediaScanner.hpp b/src/scanner/MediaScanner.hpp index 853236aa..5474a6d1 100644 --- a/src/scanner/MediaScanner.hpp +++ b/src/scanner/MediaScanner.hpp @@ -26,10 +26,11 @@ #include -#include "metadata/TagLibParser.hpp" - #include "database/ScanSettings.hpp" #include "database/DatabaseHandler.hpp" +#include "metadata/TagLibParser.hpp" + +#include "MediaScannerAddon.hpp" namespace Scanner { @@ -39,6 +40,8 @@ class MediaScanner MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool); + void setAddon(MediaScannerAddon& addon); + void start(); void stop(); void restart(); @@ -83,10 +86,10 @@ class MediaScanner // Helpers void refreshScanSettings(); - void removeMissingTracks( Stats& stats ); + void removeMissingTracks(Stats& stats); void removeOrphanEntries(); - void checkDuplicatedAudioFiles( Stats& stats ); - void scanAudioFile( const boost::filesystem::path& file, bool forceScan, Stats& stats); + void checkDuplicatedAudioFiles(Stats& stats); + void scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats); bool _running; Wt::WIOService _ioService; @@ -106,6 +109,8 @@ class MediaScanner MetaData::TagLibParser _metadataParser; + std::vector _addons; + }; // class MediaScanner } // Scanner diff --git a/src/scanner/MediaScannerAddon.hpp b/src/scanner/MediaScannerAddon.hpp new file mode 100644 index 00000000..514ab5c5 --- /dev/null +++ b/src/scanner/MediaScannerAddon.hpp @@ -0,0 +1,39 @@ +/* + * 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 "database/Types.hpp" + +namespace Scanner { + +class MediaScannerAddon +{ + public: + + virtual void refreshSettings() = 0; + + virtual void trackAdded(Database::IdType trackId) = 0; + virtual void trackToRemove(Database::IdType trackId) = 0; + virtual void trackUpdated(Database::IdType trackId) = 0; + virtual void preScanComplete() = 0; +}; + +} // ns Scanner + diff --git a/src/similarity/SimilaritySearcher.cpp b/src/similarity/SimilaritySearcher.cpp new file mode 100644 index 00000000..4f504cf8 --- /dev/null +++ b/src/similarity/SimilaritySearcher.cpp @@ -0,0 +1,58 @@ +/* + * 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 . + */ + +#include "SimilaritySearcher.hpp" + +namespace Similarity { + +Searcher::Searcher(SOMScannerAddon& somAddon) +: _somAddon(somAddon) +{} + +std::vector +Searcher::getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) +{ + auto somSearcher = _somAddon.getSearcher(); + if (!somSearcher) + return {}; + + return somSearcher->getSimilarTracks(tracksId, maxCount); +} + +std::vector +Searcher::getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount) +{ + auto somSearcher = _somAddon.getSearcher(); + if (!somSearcher) + return {}; + + return somSearcher->getSimilarReleases(session, releaseId, maxCount); +} + +std::vector +Searcher::getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount) +{ + auto somSearcher = _somAddon.getSearcher(); + if (!somSearcher) + return {}; + + return somSearcher->getSimilarArtists(session, artistId, maxCount); +} + +} // ns Similarity diff --git a/src/similarity/SimilaritySearcher.hpp b/src/similarity/SimilaritySearcher.hpp new file mode 100644 index 00000000..1ebd0839 --- /dev/null +++ b/src/similarity/SimilaritySearcher.hpp @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +#pragma once + +#include + +#include "database/Types.hpp" +#include "som/SimilaritySOMScannerAddon.hpp" + +namespace Similarity { + +class Searcher +{ + public: + Searcher(SOMScannerAddon& somAddon); + + std::vector getSimilarTracks(const std::vector& tracksId, std::size_t maxCount); + std::vector getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount); + std::vector getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount); + + private: + + SOMScannerAddon& _somAddon; +}; + +} // ns Similarity diff --git a/src/similarity/cluster/SimilarityClusterSearcher.cpp b/src/similarity/cluster/SimilarityClusterSearcher.cpp new file mode 100644 index 00000000..312fbe77 --- /dev/null +++ b/src/similarity/cluster/SimilarityClusterSearcher.cpp @@ -0,0 +1,110 @@ +/* + * 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 . + */ + +#include "SimilarityClusterSearcher.hpp" + +#include +#include + +#include "database/Cluster.hpp" +#include "database/Track.hpp" +#include "utils/Utils.hpp" + +namespace Similarity { + +std::vector +ClusterSearcher::getSimilarTracks(Wt::Dbo::Session& session, const std::vector& tracksId, std::size_t maxCount) +{ + Wt::Dbo::Transaction transaction(session); + + std::vector clusterIds; + for (auto trackId : tracksId) + { + auto track = Database::Track::getById(session, trackId); + if (!track) + continue; + + auto clusters = track->getClusters(); + if (clusters.empty()) + continue; + + for (const auto& cluster : clusters) + clusterIds.push_back(cluster.id()); + } + + std::vector sortedClusterIds; + uniqueAndSortedByOccurence(clusterIds.begin(), clusterIds.end(), std::back_inserter(sortedClusterIds)); + + +#if 0 + auto now = std::chrono::system_clock::now(); + std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); + + + std::set trackIds; + { + auto ids = tracklist->getTrackIds(); + trackIds = std::set(ids.begin(), ids.end()); + } + + // Get all the tracks of the tracklist, get the cluster that is mostly used + // and reuse it to get the next track + auto clusters = tracklist->getClusters(); + if (clusters.empty()) + return; + + for (auto cluster : clusters) + { + std::set clusterTrackIds = cluster->getTrackIds(); + + std::set candidateTrackIds; + std::set_difference(clusterTrackIds.begin(), clusterTrackIds.end(), + trackIds.begin(), trackIds.end(), + std::inserter(candidateTrackIds, candidateTrackIds.end())); + + if (candidateTrackIds.empty()) + continue; + + std::uniform_int_distribution dist(0, candidateTrackIds.size() - 1); + + auto trackToAdd = Database::Track::getById(LmsApp->getDboSession(), *std::next(candidateTrackIds.begin(), dist(randGenerator))); + enqueueTrack(trackToAdd); + + return; + } + + LMS_LOG(UI, INFO) << "No more track to be added!"; +#endif + return {}; +} + +std::vector +ClusterSearcher::getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount) +{ + + return {}; +} + +std::vector +ClusterSearcher::getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount) +{ + return {}; +} + +} // namespace Similarity diff --git a/src/similarity/cluster/SimilarityClusterSearcher.hpp b/src/similarity/cluster/SimilarityClusterSearcher.hpp new file mode 100644 index 00000000..94aab4e7 --- /dev/null +++ b/src/similarity/cluster/SimilarityClusterSearcher.hpp @@ -0,0 +1,36 @@ +/* + * 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 "database/Types.hpp" + +namespace Similarity { + +class ClusterSearcher +{ + public: + std::vector getSimilarTracks(Wt::Dbo::Session& session, const std::vector& tracksId, std::size_t maxCount); + std::vector getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount); + std::vector getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount); +}; + +} // namespace Similarity diff --git a/src/similarity/som/AcousticBrainzUtils.cpp b/src/similarity/som/AcousticBrainzUtils.cpp new file mode 100644 index 00000000..3b558ce6 --- /dev/null +++ b/src/similarity/som/AcousticBrainzUtils.cpp @@ -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 . + */ + +#include "AcousticBrainzUtils.hpp" + +#include +#include +#include + +#include "utils/Config.hpp" +#include "utils/Logger.hpp" + + +namespace AcousticBrainz +{ + + +static size_t writeToOStringStream(void *buffer, size_t size, size_t nmemb, void* ctx) +{ + std::ostringstream& oss = *reinterpret_cast(ctx); + + oss.write(reinterpret_cast(buffer), size * nmemb); + + return size * nmemb; +} + +static bool +getFeaturesFromJsonData(const std::string& jsonData, const std::set& featuresName, std::map& features) +{ + try + { + boost::property_tree::ptree root; + + std::istringstream iss(jsonData); + + boost::property_tree::read_json(iss, root); + + for (const auto& featureName : featuresName) + { + features[featureName] = root.get(featureName); + } + + return true; + } + catch (std::exception& e) + { + LMS_LOG(DBUPDATER, ERROR) << "Cannot extract feature: " << e.what(); + return false; + } +} + +static std::string +getJsonData(const std::string& mbid) +{ + static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/"; + + std::string data; + std::string url = Config::instance().getString("acousticbrainz-api-url", defaultAPIURL) + mbid + "/low-level"; + + CURL *curl; + CURLcode res; + + curl = curl_easy_init(); + if (!curl) + { + LMS_LOG(DBUPDATER, ERROR) << "CURL init failed"; + return data; + } + + std::ostringstream oss; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToOStringStream); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &oss); + + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + LMS_LOG(DBUPDATER, ERROR) << "CURL perform failed: " << curl_easy_strerror(res); + return data; + } + + curl_easy_cleanup(curl); + + data = std::move(oss.str()); + + return data; +} + +bool +extractFeatures(const std::string& mbid, const std::set& featuresName, std::map& features) +{ + return getFeaturesFromJsonData(getJsonData(mbid), featuresName, features); +} + +} // namespace Scanner::AcousticBrainz diff --git a/src/similarity/som/AcousticBrainzUtils.hpp b/src/similarity/som/AcousticBrainzUtils.hpp new file mode 100644 index 00000000..079d8ef9 --- /dev/null +++ b/src/similarity/som/AcousticBrainzUtils.hpp @@ -0,0 +1,31 @@ +/* + * 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 +#include + +namespace AcousticBrainz +{ + + bool extractFeatures(const std::string& MBID, const std::set& featuresName, std::map& features); +} + diff --git a/src/clusterer/DataNormalizer.cpp b/src/similarity/som/DataNormalizer.cpp similarity index 81% rename from src/clusterer/DataNormalizer.cpp rename to src/similarity/som/DataNormalizer.cpp index 56066618..33f0e810 100644 --- a/src/clusterer/DataNormalizer.cpp +++ b/src/similarity/som/DataNormalizer.cpp @@ -21,6 +21,7 @@ #include #include +#include namespace SOM { @@ -49,6 +50,11 @@ DataNormalizer::DataNormalizer(std::size_t inputDimCount) { } +DataNormalizer::DataNormalizer(const std::string& data) +{ + serializeFrom(data); +} + void DataNormalizer::computeNormalizationFactors(const std::vector& inputVectors) { @@ -97,6 +103,34 @@ DataNormalizer::normalizeData(InputVector& a) const } } +std::string +DataNormalizer::serializeTo() const +{ + std::ostringstream oss; + + oss << _inputDimCount << " "; + for (std::size_t i = 0; i < _inputDimCount; ++i) + oss << _minmax[i].min << " " << _minmax[i].max; + + return oss.str(); +} + +void +DataNormalizer::serializeFrom(const std::string& data) +{ + std::istringstream iss(data); + + iss >> _inputDimCount; + _minmax.resize(_inputDimCount); + + for (std::size_t i = 0; i < _inputDimCount; ++i) + { + iss >> _minmax[i].min; + iss >> _minmax[i].max; + } + +} + void DataNormalizer::dump(std::ostream& os) const { diff --git a/src/clusterer/DataNormalizer.hpp b/src/similarity/som/DataNormalizer.hpp similarity index 89% rename from src/clusterer/DataNormalizer.hpp rename to src/similarity/som/DataNormalizer.hpp index 216297c7..cf69f8e9 100644 --- a/src/clusterer/DataNormalizer.hpp +++ b/src/similarity/som/DataNormalizer.hpp @@ -22,7 +22,7 @@ #include #include -#include "SOM.hpp" +#include "Network.hpp" namespace SOM { @@ -30,15 +30,20 @@ namespace SOM class DataNormalizer { public: + DataNormalizer(std::size_t inputDimCount); + DataNormalizer(const std::string& data); void computeNormalizationFactors(const std::vector& dataSamples); void normalizeData(InputVector& data) const; + std::string serializeTo() const; + void dump(std::ostream& os) const; private: + void serializeFrom(const std::string& data); InputVector::value_type normalizeValue(InputVector::value_type value, std::size_t dimensionId) const; std::size_t _inputDimCount; diff --git a/src/similarity/som/Matrix.hpp b/src/similarity/som/Matrix.hpp new file mode 100644 index 00000000..f0899b79 --- /dev/null +++ b/src/similarity/som/Matrix.hpp @@ -0,0 +1,104 @@ +/* + * 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 +#include +#include + +namespace SOM +{ + +struct Coords +{ + std::size_t x; + std::size_t y; + + bool operator<(const Coords& other) const + { + return x < other.x && y < other.y; + } +}; + +template +class Matrix +{ + public: + + Matrix(std::size_t width, std::size_t height) + : _width(width), + _height(height) + { + _values.resize(_width*_height); + } + + Matrix(std::size_t width, std::size_t height, std::vector values) + : _width(width), + _height(height), + _values(std::move(values)) + { + assert(_values.size() == _width * _height); + } + + void clear() + { + std::vector values(_width*_height); + _values.swap(values); + } + + std::size_t getHeight() const { return _height; } + std::size_t getWidth() const { return _width; } + + T& get(Coords coords) + { + assert(coords.x < _width); + assert(coords.y < _height); + + return _values[coords.x + _width*coords.y]; + } + + const T& get(Coords coords) const + { + assert(coords.x < _width); + assert(coords.y < _height); + return _values[coords.x + _width*coords.y]; + } + + T& operator[](Coords coords) { return get(coords); } + const T& operator[](Coords coords) const { return get(coords); } + + template + Coords getCoordsMinElement(Func func) const + { + auto it = std::min_element(_values.begin(), _values.end(), func); + auto index = std::distance(_values.begin(), it); + + return {index % _height, index / _height}; + } + + private: + + std::size_t _width; + std::size_t _height; + std::vector _values; +}; + +} // ns SOM diff --git a/src/clusterer/SOM.cpp b/src/similarity/som/Network.cpp similarity index 69% rename from src/clusterer/SOM.cpp rename to src/similarity/som/Network.cpp index 0914cc60..b3bd0b5d 100644 --- a/src/clusterer/SOM.cpp +++ b/src/similarity/som/Network.cpp @@ -17,12 +17,15 @@ * along with LMS. If not, see . */ -#include "SOM.hpp" +#include "Network.hpp" -#include -#include -#include #include +#include +#include +#include +#include + +#include "utils/Logger.hpp" namespace SOM { @@ -41,7 +44,7 @@ checkSameDimensions(const InputVector& a, std::size_t inputDimCount) throw SOMException("Bad data dimension count"); } -InputVector::value_type +static InputVector::value_type defaultLearningFactor(Network::Progress progress) { constexpr InputVector::value_type initialValue = 1; @@ -49,7 +52,7 @@ defaultLearningFactor(Network::Progress progress) return initialValue * exp(-((progress.idIteration + 1) / static_cast(progress.iterationCount))); } -InputVector::value_type +static InputVector::value_type euclidianSquareDistance(const InputVector& a, const InputVector& b, const InputVector& weights) { checkSameDimensions(a, b); @@ -74,6 +77,7 @@ sigmaFunc(Network::Progress progress) return sigma0 * exp(- ((progress.idIteration + 1) / static_cast(progress.iterationCount))); } +static InputVector::value_type defaultNeighborhoodFunc(InputVector::value_type norm, Network::Progress progress) { @@ -97,7 +101,7 @@ operator<<(std::ostream& os, const InputVector& a) } -//static +static InputVector::value_type norm(const InputVector& a) { @@ -111,7 +115,7 @@ norm(const InputVector& a) return sqrt(res); } -//static +static InputVector operator+(const InputVector& a, const InputVector& b) { @@ -143,7 +147,7 @@ operator-(const InputVector& a, const InputVector& b) return res; } -//static +static InputVector operator*(const InputVector& a, InputVector::value_type factor) { @@ -157,33 +161,42 @@ operator*(const InputVector& a, InputVector::value_type factor) return res; } - Network::Network(std::size_t width, std::size_t height, std::size_t inputDimCount) -: _width(width), -_height(height), +: _inputDimCount(inputDimCount), _weights(inputDimCount, static_cast(1)), +_refVectors(width, height), _distanceFunc(euclidianSquareDistance), _learningFactorFunc(defaultLearningFactor), _neighborhoodFunc(defaultNeighborhoodFunc) { - _refVectors.resize(width * height); - auto now = std::chrono::system_clock::now(); std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); // init each vector with a random normalized value std::uniform_real_distribution dist(0, 1); - for (auto& refVector : _refVectors) + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) { - refVector.resize(inputDimCount); - - for (auto& val : refVector) - val = dist(randGenerator); + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) + { + auto& refVector = _refVectors.get({x,y}); + refVector.resize(_inputDimCount); + for (auto& val : refVector) + val = dist(randGenerator); + } } } +Network::Network(const std::string& data) +: _refVectors(0, 0), +_distanceFunc(euclidianSquareDistance), +_learningFactorFunc(defaultLearningFactor), +_neighborhoodFunc(defaultNeighborhoodFunc) +{ + serializeFrom(data); +} + void Network::setDataWeights(const InputVector& weights) { @@ -192,28 +205,16 @@ Network::setDataWeights(const InputVector& weights) _weights = weights; } -InputVector& -Network::getRefVector(std::size_t x, std::size_t y) -{ - return _refVectors[x + y*_width]; -} - -const InputVector& -Network::getRefVector(std::size_t x, std::size_t y) const -{ - return _refVectors[x + y*_width]; -} - void Network::dump(std::ostream& os) const { - os << "Width: " << _width << ", Height: " << _height << std::endl;; + os << "Width: " << _refVectors.getWidth() << ", Height: " << _refVectors.getHeight() << std::endl;; - for (std::size_t y = 0; y < _height; ++y) + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) { - for (std::size_t x = 0; x < _width; ++x) + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) { - os << getRefVector(x, y) << " "; + os << _refVectors.get({x, y}) << " "; } os << std::endl; @@ -224,15 +225,10 @@ Network::dump(std::ostream& os) const Coords Network::getClosestRefVector(const InputVector& data) const { - auto it = std::min_element(_refVectors.begin(), _refVectors.end(), - [&](const auto& a, const auto& b) + return _refVectors.getCoordsMinElement([&](const auto& a, const auto& b) { return (_distanceFunc(a, data, _weights) < _distanceFunc(b, data, _weights)); }); - - auto index = std::distance(_refVectors.begin(), it); - - return {index % _height, index / _height}; } Coords @@ -251,15 +247,15 @@ Network::classify(const InputVector& data, std::size_t size) const }; std::vector sortedEntries; - for (std::size_t x = 0; x < _width; ++x) + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) { - for (std::size_t y = 0; y < _height; ++y) + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) { - sortedEntries.push_back( Entry{{x, y}, getRefVector(x, y)} ); + sortedEntries.push_back( Entry{{x, y}, _refVectors.get({x, y})} ); } } - const InputVector& closestRefVector = getRefVector(getClosestRefVector(data)); + const InputVector& closestRefVector = _refVectors.get(getClosestRefVector(data)); std::sort(sortedEntries.begin(), sortedEntries.end(), [&](const Entry& a, const Entry& b) @@ -292,11 +288,11 @@ computeCoordsNorm(Coords c1, Coords c2) void Network::updateRefVectors(Coords closestRefVectorCoords, const InputVector& input, Progress progress) { - for (std::size_t y = 0; y < _height; ++y) + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) { - for (std::size_t x = 0; x < _width; ++x) + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) { - auto& refVector = getRefVector(x, y); + auto& refVector = _refVectors.get({x, y}); auto delta = input - refVector; auto n = computeCoordsNorm({x, y}, closestRefVectorCoords); @@ -335,6 +331,68 @@ Network::train(const std::vector& inputData, std::size_t nbIteratio } } +std::string +Network::serializeTo() const +{ + std::ostringstream oss; + + oss << _inputDimCount << " "; + + for (auto weight : _weights) + oss << weight << " "; + + // Matrix + oss << _refVectors.getWidth() << " " << _refVectors.getHeight() << " "; + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) + { + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) + { + for (auto val : _refVectors.get({x,y})) + oss << val << " "; + } + } + + return oss.str(); +} + +void +Network::serializeFrom(const std::string& data) +{ + std::istringstream iss(data); + + LMS_LOG(SIMILARITY, DEBUG) << "data = '" << data << "'"; + iss >> _inputDimCount; + LMS_LOG(SIMILARITY, DEBUG) << "Input dim count = " << _inputDimCount; + + for (std::size_t i = 0; i < _inputDimCount; ++i) + { + InputVector::value_type val; + iss >> val; + _weights.push_back(val); + } + + LMS_LOG(SIMILARITY, DEBUG) << "Reading matrix..."; + std::size_t width, height; + iss >> width >> height; + _refVectors = Matrix(width, height); + + for (std::size_t x = 0; x < _refVectors.getWidth(); ++x) + { + for (std::size_t y = 0; y < _refVectors.getHeight(); ++y) + { + InputVector refVector; + refVector.reserve(_inputDimCount); + for (std::size_t i = 0; i < _inputDimCount; ++i) + { + InputVector::value_type val; + iss >> val; + refVector.push_back(val); + } + _refVectors.get({x, y}) = refVector; + } + } +} + } // namespace SOM diff --git a/src/clusterer/SOM.hpp b/src/similarity/som/Network.hpp similarity index 85% rename from src/clusterer/SOM.hpp rename to src/similarity/som/Network.hpp index e292b02d..c6a997d9 100644 --- a/src/clusterer/SOM.hpp +++ b/src/similarity/som/Network.hpp @@ -23,6 +23,8 @@ #include #include +#include "Matrix.hpp" + #include "utils/Exception.hpp" namespace SOM @@ -39,19 +41,20 @@ class SOMException : public LmsException SOMException(const std::string& msg) : LmsException(msg) {} }; -// Top Left is (0,0) -struct Coords -{ - std::size_t x; - std::size_t y; -}; class Network { public: + // Init a network with random values Network(std::size_t width, std::size_t height, std::size_t inputDimCount); + // Init a network with serialized values + Network(const std::string& data); + + std::size_t getWidth() const { return _refVectors.getWidth(); } + std::size_t getHeight() const { return _refVectors.getHeight(); } + std::size_t getInputDimCount() const {return _inputDimCount;} // Set weight for each dimension (default is 1 for each weight) void setDataWeights(const InputVector& weights); @@ -85,21 +88,19 @@ class Network using NeighborhoodFunc = std::function; void setNeighborhoodFunc(NeighborhoodFunc neighborhoodFunc); + std::string serializeTo() const; + private: - InputVector& getRefVector(std::size_t x, std::size_t y); - const InputVector& getRefVector(std::size_t x, std::size_t y) const; - const InputVector& getRefVector(Coords coords) const { return getRefVector(coords.x, coords.y); } + void serializeFrom(const std::string& data); + Coords getClosestRefVector(const InputVector& data) const; void updateRefVectors(Coords closestRefVectorCoords, const InputVector& input, Progress progress); - std::size_t _width; - std::size_t _height; std::size_t _inputDimCount; - - InputVector _weights; - std::vector _refVectors; // reference vectors + InputVector _weights; // weight for each dimension + Matrix _refVectors; DistanceFunc _distanceFunc; LearningFactorFunc _learningFactorFunc; diff --git a/src/similarity/som/SimilaritySOMScannerAddon.cpp b/src/similarity/som/SimilaritySOMScannerAddon.cpp new file mode 100644 index 00000000..d3474095 --- /dev/null +++ b/src/similarity/som/SimilaritySOMScannerAddon.cpp @@ -0,0 +1,272 @@ +/* + * 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 . + */ + +#include "SimilaritySOMScannerAddon.hpp" + +#include + +#include "database/Track.hpp" +#include "database/SimilaritySettings.hpp" +#include "database/TrackFeature.hpp" +#include "utils/Logger.hpp" + +#include "AcousticBrainzUtils.hpp" +#include "DataNormalizer.hpp" +#include "Network.hpp" + + +namespace Similarity { + +namespace { + +struct TrackInfo +{ + Database::IdType id; + std::string mbid; +}; + +std::vector +getTracksWithMBIDAndMissingFeatures(Wt::Dbo::Session& session) +{ + std::vector res; + + Wt::Dbo::Transaction transaction(session); + + auto tracks = Database::Track::getAllWithMBIDAndMissingFeatures(session); + for (auto track : tracks) + res.push_back({track.id(), track->getMBID()}); + + return res; +} + +std::vector +getTrackFeatureTypes(Wt::Dbo::Session& session, const std::set& featureNames) +{ + std::vector res; + + for (const auto& featureName : featureNames) + { + auto trackFeatureType = Database::TrackFeatureType::getByName(session, featureName); + if (!trackFeatureType) + { + LMS_LOG(DBUPDATER, ERROR) << "Missing feature type '" << featureName << "'"; + res.clear(); + return res; + } + + res.push_back(trackFeatureType); + } + + return res; +} + + +bool +extractFeatures(const Database::Track::pointer& track, const std::vector& trackFeatureTypes, std::vector& features) +{ + features.reserve(trackFeatureTypes.size()); + + for (const auto& trackFeatureType : trackFeatureTypes) + { + auto feature = track->getTrackFeature(trackFeatureType); + if (!feature) + { + LMS_LOG(DBUPDATER, ERROR) << "Missing feature " << trackFeatureType->getName() << " for track '" << track->getPath().string() << "'"; + return false; + } + + features.emplace_back(feature->getValue()); + } + + return true; +} + + +} // namespace + +SOMScannerAddon::SOMScannerAddon(Wt::Dbo::SqlConnectionPool& connectionPool) +: _db(connectionPool) +{ + refreshSettings(); + clusterize(); +} + +std::shared_ptr +SOMScannerAddon::getSearcher() +{ + return std::atomic_load(&_finder); +} + +void +SOMScannerAddon::trackUpdated(Database::IdType trackId) +{ + Wt::Dbo::Transaction transaction(_db.getSession()); + + auto track = Database::Track::getById(_db.getSession(), trackId); + if (!track) + return; + + track.modify()->eraseFeatures(); +} + +void +SOMScannerAddon::preScanComplete() +{ + auto tracksInfo = getTracksWithMBIDAndMissingFeatures(_db.getSession()); + for (const auto& trackInfo : tracksInfo) + fetchFeatures(trackInfo.id, trackInfo.mbid); + + LMS_LOG(DBUPDATER, INFO) << "Clustering tracks..."; + clusterize(); + LMS_LOG(DBUPDATER, INFO) << "Clusterization complete!"; +} + +void +SOMScannerAddon::clusterize() +{ + Wt::Dbo::Transaction transaction(_db.getSession()); + + auto trackFeatureTypes = getTrackFeatureTypes(_db.getSession(), _featuresName); + + LMS_LOG(DBUPDATER, DEBUG) << "Getting feature types DONE..."; + + LMS_LOG(DBUPDATER, DEBUG) << "Getting Tracks with features..."; + auto tracks = Database::Track::getAllWithFeatures(_db.getSession()); + LMS_LOG(DBUPDATER, DEBUG) << "Getting Tracks with features DONE"; + + std::vector samples; + std::vector tracksIds; + + LMS_LOG(DBUPDATER, DEBUG) << "Extracting features..."; + for (auto track : tracks) + { + SOM::InputVector sample; + + if (!extractFeatures(track, trackFeatureTypes, sample)) + continue; + + samples.emplace_back(std::move(sample)); + tracksIds.emplace_back(track.id()); + } + LMS_LOG(DBUPDATER, DEBUG) << "Extracting features DONE"; + + transaction.commit(); + + if (tracksIds.empty()) + { + LMS_LOG(DBUPDATER, INFO) << "Nothing to classify!"; + std::atomic_store(&_finder, std::shared_ptr()); + return; + } + + LMS_LOG(DBUPDATER, DEBUG) << "Normalizing data..."; + SOM::DataNormalizer normalizer(_featuresName.size()); + + normalizer.computeNormalizationFactors(samples); + for (auto& sample : samples) + normalizer.normalizeData(sample); + + std::size_t size = std::sqrt(samples.size()/5); + LMS_LOG(DBUPDATER, DEBUG) << "Found " << samples.size() << " tracks, Constructing a " << size << "*" << size << " network"; + SOM::Network network(size, size, _featuresName.size()); + + LMS_LOG(DBUPDATER, DEBUG) << "Training network..."; + network.train(samples, 20); + LMS_LOG(DBUPDATER, DEBUG) << "Training network DONE"; + + // Now classify all the tracks + LMS_LOG(DBUPDATER, DEBUG) << "Classifying tracks..."; + SOM::Matrix> tracksMap(network.getWidth(), network.getHeight()); + std::map trackIdsCoords; + + for (std::size_t i = 0; i < samples.size(); ++i) + { + const auto& sample = samples[i]; + auto trackId = tracksIds[i]; + + auto coords = network.classify(sample); + tracksMap[coords].push_back(trackId); + trackIdsCoords[trackId] = coords; + } + + Similarity::SOMSearcher::ConstructionParams params{std::move(network), std::move(normalizer), std::move(tracksMap), std::move(trackIdsCoords)}; + + auto finder = std::make_shared(std::move(params)); + + std::atomic_store(&_finder, finder); + + LMS_LOG(DBUPDATER, DEBUG) << "Classifying tracks DONE"; + + LMS_LOG(DBUPDATER, DEBUG) << "Dumping classifier:"; + + std::ofstream ofs("/tmp/output"); + finder->dump(_db.getSession(), ofs); + + LMS_LOG(DBUPDATER, DEBUG) << "Dumping classifier DONE"; + +} + +void +SOMScannerAddon::refreshSettings() +{ + Wt::Dbo::Transaction transaction(_db.getSession()); + + auto settings = Database::SimilaritySettings::get(_db.getSession()); + + _settingsVersion = settings->getVersion(); + + for (auto trackFeatureType : settings->getTrackFeatureTypes()) + { + _featuresName.insert(trackFeatureType->getName()); + } +} + +bool +SOMScannerAddon::fetchFeatures(Database::IdType trackId, const std::string& MBID) +{ + std::map features; + + if (!AcousticBrainz::extractFeatures(MBID, _featuresName, features)) + { + LMS_LOG(DBUPDATER, ERROR) << "Cannot extract features using AcousticBrainz!"; + return false; + } + + Wt::Dbo::Transaction transaction(_db.getSession()); + + Wt::Dbo::ptr track = Database::Track::getById(_db.getSession(), trackId); + if (!track) + return false; + + LMS_LOG(DBUPDATER, DEBUG) << "Successfully extracted AcousticBrainz lowlevel features for track '" << track->getPath().string() << "'"; + + for (const auto& feature : features) + { + auto featureType = Database::TrackFeatureType::getByName(_db.getSession(), feature.first); + if (!featureType) + return false; + + Database::TrackFeature::create(_db.getSession(), featureType, track, feature.second); + } + + return true; +} + +} // namespace Similarity + diff --git a/src/similarity/som/SimilaritySOMScannerAddon.hpp b/src/similarity/som/SimilaritySOMScannerAddon.hpp new file mode 100644 index 00000000..c3ec01c7 --- /dev/null +++ b/src/similarity/som/SimilaritySOMScannerAddon.hpp @@ -0,0 +1,61 @@ +/* + * 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 "database/DatabaseHandler.hpp" +#include "scanner/MediaScannerAddon.hpp" + +#include "SimilaritySOMSearcher.hpp" + +namespace Similarity { + +class SOMScannerAddon : public Scanner::MediaScannerAddon +{ + public: + + SOMScannerAddon(Wt::Dbo::SqlConnectionPool& connectionPool); + + std::shared_ptr getSearcher(); + + private: + void refreshSettings() override; + void trackAdded(Database::IdType trackId) override {} + void trackToRemove(Database::IdType trackId) override {} + void trackUpdated(Database::IdType trackId) override; + void preScanComplete() override; + + bool fetchFeatures(Database::IdType trackId, const std::string& MBID); + + void clusterize(); + + std::size_t _settingsVersion; + std::set _featuresName; + Database::Handler _db; + + std::shared_ptr _finder; +}; + +SOMScannerAddon* setSOMScannerAddon(SOMScannerAddon addon); +SOMScannerAddon* getSOMScannerAddon(); + +} // namespace Similarity + diff --git a/src/similarity/som/SimilaritySOMSearcher.cpp b/src/similarity/som/SimilaritySOMSearcher.cpp new file mode 100644 index 00000000..7b751b6f --- /dev/null +++ b/src/similarity/som/SimilaritySOMSearcher.cpp @@ -0,0 +1,259 @@ +/* + * 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 . + */ + +#include "SimilaritySOMSearcher.hpp" + +#include + +#include "database/Artist.hpp" +#include "database/SimilaritySettings.hpp" +#include "database/Release.hpp" +#include "database/Track.hpp" +#include "utils/Logger.hpp" +#include "utils/Utils.hpp" + +namespace Similarity { + +SOMSearcher::SOMSearcher(ConstructionParams params) +: _network(std::move(params.network)), +_normalizer(std::move(params.normalizer)), +_tracksMap(std::move(params.tracksMap)), +_trackIdsCoords(std::move(params.trackIdsCoords)) +{ +} + +std::vector +SOMSearcher::getSimilarTracks(const std::vector& tracksIds, std::size_t maxCount) +{ + std::vector res; + + auto bestCoords = getBestMatchingCoords(tracksIds); + if (!bestCoords) + return res; + + auto tracks = _tracksMap[*bestCoords]; + + auto now = std::chrono::system_clock::now(); + std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); + + std::shuffle(tracks.begin(), tracks.end(), randGenerator); + + if (tracks.size() > maxCount) + tracks.resize(maxCount); + + return tracks; +} + +std::vector +SOMSearcher::getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount) +{ + std::vector res; + + Wt::Dbo::Transaction transaction(session); + + auto release = Database::Release::getById(session, releaseId); + if (!release) + return res; + + auto tracks = release->getTracks(); + + std::vector tracksIds; + for (auto track : tracks) + tracksIds.push_back(track.id()); + + auto matchingCoords = getMatchingCoords(tracksIds); + if (matchingCoords.empty()) + return res; + + auto releases = getReleases(session, matchingCoords); + uniqueAndSortedByOccurence(releases.begin(), releases.end(), std::back_inserter(res)); + + res.erase(std::remove_if(res.begin(), res.end(), [&](auto releaseId) { return releaseId == release.id(); }), res.end()); + + if (res.size() > maxCount) + res.resize(maxCount); + + LMS_LOG(SIMILARITY, DEBUG) << "*** SIMILARITY RESULT *** :"; + for (auto id : res) + LMS_LOG(SIMILARITY, DEBUG) << id; + + return res; +} + +std::vector +SOMSearcher::getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount) +{ + std::vector res; + + Wt::Dbo::Transaction transaction(session); + + auto artist = Database::Artist::getById(session, artistId); + if (!artist) + return res; + + auto tracks = artist->getTracks(); + + std::vector tracksIds; + for (auto track : tracks) + tracksIds.push_back(track.id()); + + auto matchingCoords = getMatchingCoords(tracksIds); + if (matchingCoords.empty()) + return res; + + auto artists = getArtists(session, matchingCoords); + uniqueAndSortedByOccurence(artists.begin(), artists.end(), std::back_inserter(res)); + + res.erase(std::remove_if(res.begin(), res.end(), [&](auto artistId) { return artistId == artist.id(); }), res.end()); + + if (res.size() > maxCount) + res.resize(maxCount); + + LMS_LOG(SIMILARITY, DEBUG) << "*** SIMILARITY RESULT *** :"; + for (auto id : res) + LMS_LOG(SIMILARITY, DEBUG) << id; + + return res; +} + +void +SOMSearcher::dump(Wt::Dbo::Session& session, std::ostream& os) const +{ + os << "Number of tracks classified: " << _trackIdsCoords.size() << std::endl; + os << "Network size: " << _network.getWidth() << " * " << _network.getHeight() << std::endl; + + Wt::Dbo::Transaction transaction(session); + + for (std::size_t y = 0; y < _network.getHeight(); ++y) + { + for (std::size_t x = 0; x < _network.getWidth(); ++x) + { + const auto& trackIds = _tracksMap[{x, y}]; + + for (auto trackId : trackIds) + { + auto track = Database::Track::getById(session, trackId); + if (!track) + continue; + + os << "{"; + if (track->getArtist()) + os << track->getArtist()->getName() << " "; + if (track->getRelease()) + os << track->getRelease()->getName(); + os << "} "; + } + + os << "; "; + } + os << std::endl; + } +} + +boost::optional +SOMSearcher::getBestMatchingCoords(const std::vector& tracksIds) const +{ + if (tracksIds.empty()) + return boost::none; + + std::map coordsCount; + + for (auto trackId : tracksIds) + { + auto it = _trackIdsCoords.find(trackId); + if (it == _trackIdsCoords.end()) + continue; + + if (coordsCount.find(it->second) == coordsCount.end()) + coordsCount[it->second] = 0; + + coordsCount[it->second]++; + } + + if (coordsCount.empty()) + return boost::none; + + auto bestCoords = std::max_element(std::begin(coordsCount), std::end(coordsCount), + [](const auto& a, const auto& b) + { + return a.second < b.second; + }); + + return bestCoords->first; +} + +std::vector +SOMSearcher::getMatchingCoords(const std::vector& tracksIds) const +{ + std::vector res; + + if (tracksIds.empty()) + return res; + + for (auto trackId : tracksIds) + { + auto it = _trackIdsCoords.find(trackId); + if (it == _trackIdsCoords.end()) + continue; + + res.push_back(it->second); + } + + return res; +} + +std::vector +SOMSearcher::getReleases(Wt::Dbo::Session& session, const std::vector& coords) const +{ + std::vector res; + for (const auto& c : coords) + { + for (auto trackId : _tracksMap[c]) + { + auto track = Database::Track::getById(session, trackId); + if (!track || !track->getRelease()) + continue; + + res.emplace_back(track->getRelease().id()); + } + } + + return res; +} + +std::vector +SOMSearcher::getArtists(Wt::Dbo::Session& session, const std::vector& coords) const +{ + std::vector res; + for (const auto& c : coords) + { + for (auto trackId : _tracksMap[c]) + { + auto track = Database::Track::getById(session, trackId); + if (!track || !track->getArtist()) + continue; + + res.emplace_back(track->getArtist().id()); + } + } + + return res; +} + +} // ns Similarity diff --git a/src/similarity/som/SimilaritySOMSearcher.hpp b/src/similarity/som/SimilaritySOMSearcher.hpp new file mode 100644 index 00000000..17d7be50 --- /dev/null +++ b/src/similarity/som/SimilaritySOMSearcher.hpp @@ -0,0 +1,65 @@ +/* + * 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 + +#include "database/DatabaseHandler.hpp" +#include "database/Types.hpp" +#include "DataNormalizer.hpp" +#include "Network.hpp" + +namespace Similarity { + +class SOMSearcher +{ + public: + + struct ConstructionParams + { + SOM::Network network; + SOM::DataNormalizer normalizer; + SOM::Matrix> tracksMap; + std::map trackIdsCoords; + }; + + SOMSearcher(ConstructionParams params); + + std::vector getSimilarTracks(const std::vector& tracksId, std::size_t maxCount); + std::vector getSimilarReleases(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t maxCount); + std::vector getSimilarArtists(Wt::Dbo::Session& session, Database::IdType artistId, std::size_t maxCount); + + void dump(Wt::Dbo::Session& session, std::ostream& os) const; + + private: + boost::optional getBestMatchingCoords(const std::vector& tracksIds) const; + std::vector getMatchingCoords(const std::vector& tracksIds) const; + + std::vector getReleases(Wt::Dbo::Session& session, const std::vector& coords) const; + std::vector getArtists(Wt::Dbo::Session& session, const std::vector& coords) const; + + SOM::Network _network; + SOM::DataNormalizer _normalizer; + SOM::Matrix> _tracksMap; + std::map _trackIdsCoords; +}; + +} // ns Similarity diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index a37e2395..9e95055d 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -31,33 +31,33 @@ #include #include "config/config.h" +#include "cover/CoverArtGrabber.hpp" +#include "database/Artist.hpp" +#include "database/Cluster.hpp" +#include "database/Release.hpp" +#include "explore/Explore.hpp" +#include "main/Services.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" -#include "explore/Explore.hpp" -#include "MediaPlayer.hpp" -#include "PlayHistoryView.hpp" -#include "PlayQueueView.hpp" -#include "SettingsView.hpp" - #include "admin/InitWizardView.hpp" #include "admin/DatabaseSettingsView.hpp" #include "admin/UserView.hpp" #include "admin/UsersView.hpp" - #include "resource/ImageResource.hpp" #include "resource/AudioResource.hpp" +#include "MediaPlayer.hpp" +#include "PlayHistoryView.hpp" +#include "PlayQueueView.hpp" +#include "SettingsView.hpp" + namespace UserInterface { std::unique_ptr -LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner) +LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups) { - /* - * You could read information from the environment to decide whether - * the user has permission to start a new application - */ - return std::make_unique(env, connectionPool, appGroups, scanner); + return std::make_unique(env, connectionPool, appGroups); } LmsApplication* @@ -66,19 +66,12 @@ LmsApplication::instance() return reinterpret_cast(Wt::WApplication::instance()); } -/* - * The env argument contains information about the new session, and - * the initial request. It must be passed to the Wt::WApplication - * constructor so it is typically also an argument for your custom - * application constructor. -*/ -LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner) +LmsApplication::LmsApplication(const Wt::WEnvironment& env, + Wt::Dbo::SqlConnectionPool& connectionPool, + LmsApplicationGroupContainer& appGroups) : Wt::WApplication(env), _db(connectionPool), - _appGroups(appGroups), - _scanner(scanner), - _imageResource(nullptr), - _audioResource(nullptr) + _appGroups(appGroups) { auto bootstrapTheme = std::make_unique(); bootstrapTheme->setVersion(Wt::BootstrapVersion::v3); @@ -96,6 +89,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti messageResourceBundle().use(appRoot() + "admin-users"); messageResourceBundle().use(appRoot() + "admin-initwizard"); messageResourceBundle().use(appRoot() + "artist"); + messageResourceBundle().use(appRoot() + "artistinfo"); + messageResourceBundle().use(appRoot() + "artistlink"); messageResourceBundle().use(appRoot() + "artists"); messageResourceBundle().use(appRoot() + "artistsinfo"); messageResourceBundle().use(appRoot() + "explore"); @@ -105,6 +100,8 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti messageResourceBundle().use(appRoot() + "playqueue"); messageResourceBundle().use(appRoot() + "playhistory"); messageResourceBundle().use(appRoot() + "release"); + messageResourceBundle().use(appRoot() + "releaseinfo"); + messageResourceBundle().use(appRoot() + "releaselink"); messageResourceBundle().use(appRoot() + "releases"); messageResourceBundle().use(appRoot() + "releasesinfo"); messageResourceBundle().use(appRoot() + "settings"); @@ -112,6 +109,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti messageResourceBundle().use(appRoot() + "tracks"); messageResourceBundle().use(appRoot() + "tracksinfo"); + // hack since Server does not expose the docRoot + getServices().coverArtGrabber->setDefaultCover(Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg"); + // Require js here to avoid async problems requireJQuery("/js/jquery-1.10.2.min.js"); require("/js/mediaplayer.js"); @@ -462,7 +462,7 @@ LmsApplication::createHome() // Events from MediaScanner std::string sessionId = LmsApp->sessionId(); - _scanner.scanComplete().connect([=] (Scanner::MediaScanner::Stats stats) + getServices().mediaScanner->scanComplete().connect([=] (Scanner::MediaScanner::Stats stats) { // Runs from media scanner context Wt::WServer::instance()->post(sessionId, [=] diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index 7d0ca982..a8d296e1 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -20,25 +20,29 @@ #ifndef LMS_APPLICATION_HPP #define LMS_APPLICATION_HPP +#include + #include #include #include "database/DatabaseHandler.hpp" #include "scanner/MediaScanner.hpp" -#include "database/Artist.hpp" -#include "database/Cluster.hpp" -#include "database/Release.hpp" - #include "LmsApplicationGroup.hpp" #include "Auth.hpp" +namespace Database { + class Artist; + class Cluster; + class Release; +} + namespace UserInterface { class AudioResource; class ImageResource; -// Events that can be listen to anywhere in the application +// Events that can be listen from anywhere in the application struct Events { // Events relative to group @@ -67,10 +71,10 @@ enum class MsgType class LmsApplication : public Wt::WApplication { public: - LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner); + LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups); static std::unique_ptr create(const Wt::WEnvironment& env, - Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner); + Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups); static LmsApplication* instance(); // Session application data @@ -83,8 +87,6 @@ class LmsApplication : public Wt::WApplication Database::User::pointer getUser() { return _db.getCurrentUser(); } Wt::WString getUserIdentity() { return _userIdentity; } - Scanner::MediaScanner& getMediaScanner() { return _scanner; } - Events& getEvents() { return _events; } // Utils @@ -94,11 +96,11 @@ class LmsApplication : public Wt::WApplication void post(std::function func); void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds(4000)); - static Wt::WLink createArtistLink(Database::Artist::pointer artist); - static std::unique_ptr createArtistAnchor(Database::Artist::pointer artist, bool addText = true); - static Wt::WLink createReleaseLink(Database::Release::pointer release); - static std::unique_ptr createReleaseAnchor(Database::Release::pointer release, bool addText = true); - static std::unique_ptr createCluster(Database::Cluster::pointer cluster, bool canDelete = false); + static Wt::WLink createArtistLink(Wt::Dbo::ptr artist); + static std::unique_ptr createArtistAnchor(Wt::Dbo::ptr artist, bool addText = true); + static Wt::WLink createReleaseLink(Wt::Dbo::ptr release); + static std::unique_ptr createReleaseAnchor(Wt::Dbo::ptr release, bool addText = true); + static std::unique_ptr createCluster(Wt::Dbo::ptr cluster, bool canDelete = false); // Signal emitted just before the session ends (user may already be logged out) Wt::Signal<>& preQuit() { return _preQuit; } @@ -119,8 +121,7 @@ class LmsApplication : public Wt::WApplication LmsApplicationGroupContainer& _appGroups; Events _events; Wt::WString _userIdentity; - Auth* _auth; - Scanner::MediaScanner& _scanner; + Auth* _auth = nullptr; std::shared_ptr _imageResource; std::shared_ptr _audioResource; bool _isAdmin = false; diff --git a/src/ui/MediaPlayer.cpp b/src/ui/MediaPlayer.cpp index 6cceaaf1..832ee77f 100644 --- a/src/ui/MediaPlayer.cpp +++ b/src/ui/MediaPlayer.cpp @@ -22,9 +22,10 @@ #include "av/AvInfo.hpp" #include "utils/Logger.hpp" +#include "database/Artist.hpp" +#include "database/Release.hpp" #include "database/Track.hpp" - #include "resource/ImageResource.hpp" #include "resource/AudioResource.hpp" diff --git a/src/ui/PlayQueueView.cpp b/src/ui/PlayQueueView.cpp index 0ee9fd88..52168206 100644 --- a/src/ui/PlayQueueView.cpp +++ b/src/ui/PlayQueueView.cpp @@ -19,15 +19,12 @@ #include "PlayQueueView.hpp" -#include - -#include #include -#include "utils/Logger.hpp" - #include "database/TrackList.hpp" - +#include "main/Services.hpp" +#include "similarity/SimilaritySearcher.hpp" +#include "utils/Logger.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -385,45 +382,26 @@ PlayQueue::addSome() void PlayQueue::addRadioTrack() { - auto now = std::chrono::system_clock::now(); - std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); - auto tracklist = getTrackList(); - std::set trackIds; - { - auto ids = tracklist->getTrackIds(); - trackIds = std::set(ids.begin(), ids.end()); - } - - // Get all the tracks of the tracklist, get the cluster that is mostly used - // and reuse it to get the next track - auto clusters = tracklist->getClusters(); - if (clusters.empty()) + std::vector trackIds = getTrackList()->getTrackIds(); + if (trackIds.empty()) return; - for (auto cluster : clusters) + auto res = getServices().similaritySearcher->getSimilarTracks(trackIds, 1); + for (auto trackId : res) { - std::set clusterTrackIds = cluster->getTrackIds(); - - std::set candidateTrackIds; - std::set_difference(clusterTrackIds.begin(), clusterTrackIds.end(), - trackIds.begin(), trackIds.end(), - std::inserter(candidateTrackIds, candidateTrackIds.end())); - - if (candidateTrackIds.empty()) - continue; - - std::uniform_int_distribution dist(0, candidateTrackIds.size() - 1); - - auto trackToAdd = Database::Track::getById(LmsApp->getDboSession(), *std::next(candidateTrackIds.begin(), dist(randGenerator))); + auto trackToAdd = Database::Track::getById(LmsApp->getDboSession(), trackId); enqueueTrack(trackToAdd); - - return; } - LMS_LOG(UI, INFO) << "No more track to be added!"; } +void addRadioTrackFromSimilarity(std::shared_ptr similaritySearcher) +{ + +} + + } // namespace UserInterface diff --git a/src/ui/PlayQueueView.hpp b/src/ui/PlayQueueView.hpp index be0f7b19..56e3b2fd 100644 --- a/src/ui/PlayQueueView.hpp +++ b/src/ui/PlayQueueView.hpp @@ -31,6 +31,11 @@ #include "database/TrackList.hpp" #include "database/Track.hpp" +namespace Similarity +{ + class Finder; +} + namespace UserInterface { class PlayQueue : public Wt::WTemplate @@ -69,6 +74,9 @@ class PlayQueue : public Wt::WTemplate void load(std::size_t pos, bool play); void stop(); + void addRadioTrackFromSimilarity(std::shared_ptr similarityFinder); + void addRadioTrackFromClusters(); + bool _repeatAll = false; bool _radioMode = false; boost::optional _tracklistId; diff --git a/src/ui/admin/DatabaseSettingsView.cpp b/src/ui/admin/DatabaseSettingsView.cpp index d23c0cc8..65ec7a6c 100644 --- a/src/ui/admin/DatabaseSettingsView.cpp +++ b/src/ui/admin/DatabaseSettingsView.cpp @@ -19,19 +19,20 @@ #include "DatabaseSettingsView.hpp" -#include -#include #include +#include #include +#include +#include +#include #include -#include -#include - -#include "common/Validators.hpp" +#include "database/Cluster.hpp" +#include "main/Services.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" +#include "common/Validators.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -287,7 +288,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - LmsApp->getMediaScanner().reschedule(); + getServices().mediaScanner->reschedule(); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -304,7 +305,7 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] () { - LmsApp->getMediaScanner().scheduleImmediateScan(); + getServices().mediaScanner->scheduleImmediateScan(); LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched")); }); diff --git a/src/ui/explore/ArtistInfoView.cpp b/src/ui/explore/ArtistInfoView.cpp new file mode 100644 index 00000000..dc979220 --- /dev/null +++ b/src/ui/explore/ArtistInfoView.cpp @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +#include "ArtistInfoView.hpp" + +#include "database/Artist.hpp" +#include "main/Services.hpp" +#include "similarity/SimilaritySearcher.hpp" +#include "utils/Utils.hpp" + +#include "ArtistLink.hpp" +#include "LmsApplication.hpp" + +using namespace Database; + +namespace UserInterface { + +ArtistInfo::ArtistInfo() +: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistInfo.template")) +{ + addFunction("tr", &Wt::WTemplate::Functions::tr); + + _similarArtistsContainer = bindNew("similar-artists"); + + wApp->internalPathChanged().connect(std::bind([=] + { + refresh(); + })); + + LmsApp->getEvents().dbScanned.connect([=] + { + refresh(); + }); + + refresh(); +} + +void +ArtistInfo::refresh() +{ + _similarArtistsContainer->clear(); + + if (!wApp->internalPathMatches("/artist/")) + return; + + auto artistId = readAs(wApp->internalPathNextPart("/artist/")); + if (!artistId) + return; + + auto artistsIds = getServices().similaritySearcher->getSimilarArtists(LmsApp->getDboSession(), *artistId, 5); + + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + + std::vector artists; + for (auto artistId : artistsIds) + { + auto artist = Database::Artist::getById(LmsApp->getDboSession(), artistId); + + if (artist) + artists.push_back(artist); + } + + for (auto artist : artists) + _similarArtistsContainer->addNew(artist); +} + +} // namespace UserInterface + diff --git a/src/ui/explore/ArtistInfoView.hpp b/src/ui/explore/ArtistInfoView.hpp new file mode 100644 index 00000000..0d9bc51f --- /dev/null +++ b/src/ui/explore/ArtistInfoView.hpp @@ -0,0 +1,39 @@ +/* + * 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 + +namespace UserInterface { + +class ArtistInfo : public Wt::WTemplate +{ + public: + ArtistInfo(); + + private: + void refresh(); + + Wt::WContainerWidget* _similarArtistsContainer; +}; + +} // namespace UserInterface + diff --git a/src/ui/explore/ArtistLink.cpp b/src/ui/explore/ArtistLink.cpp new file mode 100644 index 00000000..2950b0b5 --- /dev/null +++ b/src/ui/explore/ArtistLink.cpp @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +#include "ArtistLink.hpp" + +#include + +#include "database/Artist.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface { + +ArtistLink::ArtistLink(Database::Artist::pointer artist) +: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ArtistLink.template")) +{ + bindWidget("name", LmsApplication::createArtistAnchor(artist)); +} + +} // namespace UserInterface diff --git a/src/ui/explore/ArtistLink.hpp b/src/ui/explore/ArtistLink.hpp new file mode 100644 index 00000000..c6ee303d --- /dev/null +++ b/src/ui/explore/ArtistLink.hpp @@ -0,0 +1,37 @@ +/* + * 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 + +namespace Database{ + class Artist; +} + +namespace UserInterface { + +class ArtistLink : public Wt::WTemplate +{ + public: + ArtistLink(Wt::Dbo::ptr artist); + +}; + +} diff --git a/src/ui/explore/ArtistView.cpp b/src/ui/explore/ArtistView.cpp index 4c790fa4..fdac71cf 100644 --- a/src/ui/explore/ArtistView.cpp +++ b/src/ui/explore/ArtistView.cpp @@ -25,6 +25,7 @@ #include #include "database/Artist.hpp" +#include "database/Release.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" diff --git a/src/ui/explore/ArtistsInfoView.cpp b/src/ui/explore/ArtistsInfoView.cpp index d64efadf..a7874501 100644 --- a/src/ui/explore/ArtistsInfoView.cpp +++ b/src/ui/explore/ArtistsInfoView.cpp @@ -19,34 +19,16 @@ #include "ArtistsInfoView.hpp" -#include #include #include "database/Artist.hpp" #include "database/TrackList.hpp" - #include "utils/Utils.hpp" - +#include "ArtistLink.hpp" #include "LmsApplication.hpp" using namespace Database; -namespace { - -using namespace UserInterface; - -void addEntries(Wt::WContainerWidget *container, const std::vector& artists) -{ - for (auto artist : artists) - { - Wt::WTemplate* entry = container->addNew(Wt::WString::tr("Lms.Explore.ArtistsInfo.template.entry")); - - entry->bindWidget("name", LmsApplication::createArtistAnchor(artist)); - } -} - -} - namespace UserInterface { ArtistsInfo::ArtistsInfo() @@ -80,7 +62,8 @@ ArtistsInfo::refreshRecentlyAdded() auto artists = Artist::getLastAdded(LmsApp->getDboSession(), after, 5); _recentlyAddedContainer->clear(); - addEntries(_recentlyAddedContainer, artists); + for (auto artist : artists) + _recentlyAddedContainer->addNew(artist); } void @@ -90,7 +73,8 @@ ArtistsInfo::refreshMostPlayed() auto artists = LmsApp->getUser()->getPlayedTrackList()->getTopArtists(5); _mostPlayedContainer->clear(); - addEntries(_mostPlayedContainer, artists); + for (auto artist : artists) + _mostPlayedContainer->addNew(artist); } } // namespace UserInterface diff --git a/src/ui/explore/Explore.cpp b/src/ui/explore/Explore.cpp index 7adbe212..b2e45ea0 100644 --- a/src/ui/explore/Explore.cpp +++ b/src/ui/explore/Explore.cpp @@ -23,14 +23,18 @@ #include #include +#include "database/Artist.hpp" +#include "database/Release.hpp" #include "utils/Logger.hpp" #include "LmsApplication.hpp" +#include "ArtistInfoView.hpp" #include "ArtistsInfoView.hpp" #include "ArtistsView.hpp" #include "ArtistView.hpp" #include "Filters.hpp" +#include "ReleaseInfoView.hpp" #include "ReleasesInfoView.hpp" #include "ReleasesView.hpp" #include "ReleaseView.hpp" @@ -79,7 +83,9 @@ handleInfoPathChange(Wt::WStackedWidget* stack) { enum Idx { - IdxArtists = 0, + IdxArtist = 0, + IdxArtists, + IdxRelease, IdxReleases, IdxTracks, }; @@ -87,9 +93,9 @@ handleInfoPathChange(Wt::WStackedWidget* stack) static const std::map indexes = { { "/artists", IdxArtists }, - { "/artist", IdxArtists }, + { "/artist", IdxArtist }, { "/releases", IdxReleases }, - { "/release", IdxReleases }, + { "/release", IdxRelease }, { "/tracks", IdxTracks }, }; @@ -151,9 +157,15 @@ Explore::Explore() // Info Wt::WStackedWidget* infoStack = bindNew("info"); + auto artistInfo = std::make_unique(); + infoStack->addWidget(std::move(artistInfo)); + auto artistsInfo = std::make_unique(); infoStack->addWidget(std::move(artistsInfo)); + auto releaseInfo = std::make_unique(); + infoStack->addWidget(std::move(releaseInfo)); + auto releasesInfo = std::make_unique(); infoStack->addWidget(std::move(releasesInfo)); diff --git a/src/ui/explore/Filters.cpp b/src/ui/explore/Filters.cpp index f91587c6..eccaa779 100644 --- a/src/ui/explore/Filters.cpp +++ b/src/ui/explore/Filters.cpp @@ -24,6 +24,8 @@ #include #include +#include "database/Cluster.hpp" + #include "LmsApplication.hpp" namespace UserInterface { diff --git a/src/ui/explore/ReleaseInfoView.cpp b/src/ui/explore/ReleaseInfoView.cpp new file mode 100644 index 00000000..d003a14d --- /dev/null +++ b/src/ui/explore/ReleaseInfoView.cpp @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +#include "ReleaseInfoView.hpp" + +#include "database/Release.hpp" +#include "main/Services.hpp" +#include "similarity/SimilaritySearcher.hpp" +#include "utils/Utils.hpp" + +#include "ReleaseLink.hpp" +#include "LmsApplication.hpp" + +using namespace Database; + +namespace UserInterface { + +ReleaseInfo::ReleaseInfo() +: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseInfo.template")) +{ + addFunction("tr", &Wt::WTemplate::Functions::tr); + + _similarReleasesContainer = bindNew("similar-releases"); + + wApp->internalPathChanged().connect(std::bind([=] + { + refresh(); + })); + + LmsApp->getEvents().dbScanned.connect([=] + { + refresh(); + }); + + refresh(); +} + +void +ReleaseInfo::refresh() +{ + _similarReleasesContainer->clear(); + + if (!wApp->internalPathMatches("/release/")) + return; + + auto releaseId = readAs(wApp->internalPathNextPart("/release/")); + if (!releaseId) + return; + + auto releasesIds = getServices().similaritySearcher->getSimilarReleases(LmsApp->getDboSession(), *releaseId, 5); + + Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + + std::vector releases; + for (auto releaseId : releasesIds) + { + auto release = Database::Release::getById(LmsApp->getDboSession(), releaseId); + + if (release) + releases.push_back(release); + } + + for (auto release : releases) + _similarReleasesContainer->addNew(release); +} + +} // namespace UserInterface + diff --git a/src/ui/explore/ReleaseInfoView.hpp b/src/ui/explore/ReleaseInfoView.hpp new file mode 100644 index 00000000..a8eb7365 --- /dev/null +++ b/src/ui/explore/ReleaseInfoView.hpp @@ -0,0 +1,39 @@ +/* + * 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 + +namespace UserInterface { + +class ReleaseInfo : public Wt::WTemplate +{ + public: + ReleaseInfo(); + + private: + void refresh(); + + Wt::WContainerWidget* _similarReleasesContainer; +}; + +} // namespace UserInterface + diff --git a/src/ui/explore/ReleaseLink.cpp b/src/ui/explore/ReleaseLink.cpp new file mode 100644 index 00000000..b036152b --- /dev/null +++ b/src/ui/explore/ReleaseLink.cpp @@ -0,0 +1,60 @@ +/* + * 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 . + */ + +#include "ReleaseLink.hpp" + +#include +#include + +#include "database/Release.hpp" +#include "resource/ImageResource.hpp" +#include "utils/Utils.hpp" + +#include "LmsApplication.hpp" + +using namespace Database; + +namespace UserInterface { + +ReleaseLink::ReleaseLink(Database::Release::pointer release) +: Wt::WTemplate(Wt::WString::tr("Lms.Explore.ReleaseLink.template")) +{ + bindWidget("release-name", LmsApplication::createReleaseAnchor(release)); + + Wt::WAnchor* anchor = bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); + auto cover = std::make_unique(); + cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 48)); + cover->setWidth(48); + anchor->setImage(std::move(cover)); + + auto artists = release->getArtists(); + if (artists.size() > 1) + { + setCondition("if-has-artist", true); + bindString("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); + } + else if (artists.size() == 1) + { + setCondition("if-has-artist", true); + bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); + } +} + +} // namespace UserInterface + diff --git a/src/ui/explore/ReleaseLink.hpp b/src/ui/explore/ReleaseLink.hpp new file mode 100644 index 00000000..9dc4a50c --- /dev/null +++ b/src/ui/explore/ReleaseLink.hpp @@ -0,0 +1,37 @@ +/* + * 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 + +namespace Database { + class Release; +} + +namespace UserInterface { + +class ReleaseLink : public Wt::WTemplate +{ + public: + ReleaseLink(Wt::Dbo::ptr); +}; + +} // namespace UserInterface + diff --git a/src/ui/explore/ReleasesInfoView.cpp b/src/ui/explore/ReleasesInfoView.cpp index 994f2fe5..2c5effce 100644 --- a/src/ui/explore/ReleasesInfoView.cpp +++ b/src/ui/explore/ReleasesInfoView.cpp @@ -19,54 +19,16 @@ #include "ReleasesInfoView.hpp" -#include -#include #include -#include #include "database/Release.hpp" #include "database/TrackList.hpp" - #include "resource/ImageResource.hpp" - +#include "ReleaseLink.hpp" #include "LmsApplication.hpp" using namespace Database; -namespace { - -using namespace UserInterface; - -void addEntries(Wt::WContainerWidget* container, const std::vector& releases) -{ - for (auto release : releases) - { - Wt::WTemplate* entry = container->addNew(Wt::WString::tr("Lms.Explore.ReleasesInfo.template.entry")); - - entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release)); - - Wt::WAnchor* anchor = entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false)); - auto cover = std::make_unique(); - cover->setImageLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 48)); - cover->setWidth(48); - anchor->setImage(std::move(cover)); - - auto artists = release->getArtists(); - if (artists.size() > 1) - { - entry->setCondition("if-has-artist", true); - entry->bindString("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); - } - else if (artists.size() == 1) - { - entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); - } - } -} - -} // namespace - namespace UserInterface { ReleasesInfo::ReleasesInfo() @@ -101,7 +63,8 @@ ReleasesInfo::refreshRecentlyAdded() auto releases = Release::getLastAdded(LmsApp->getDboSession(), after, 5); _recentlyAddedContainer->clear(); - addEntries(_recentlyAddedContainer, releases); + for (auto release : releases) + _recentlyAddedContainer->addNew(release); } void @@ -112,7 +75,8 @@ ReleasesInfo::refreshMostPlayed() auto releases = LmsApp->getUser()->getPlayedTrackList()->getTopReleases(5); _mostPlayedContainer->clear(); - addEntries(_mostPlayedContainer, releases); + for (auto release : releases) + _mostPlayedContainer->addNew(release); } } // namespace UserInterface diff --git a/src/ui/resource/ImageResource.cpp b/src/ui/resource/ImageResource.cpp index ea2863a4..46bf0e1e 100644 --- a/src/ui/resource/ImageResource.cpp +++ b/src/ui/resource/ImageResource.cpp @@ -22,16 +22,15 @@ #include #include +#include "cover/CoverArtGrabber.hpp" +#include "database/Track.hpp" +#include "main/Services.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" -#include "database/Track.hpp" - #include "LmsApplication.hpp" -#include "cover/CoverArtGrabber.hpp" - namespace UserInterface { static const std::string unknownCoverPath = "/images/unknown-cover.jpg"; @@ -81,7 +80,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // transactions are not thread safe { Wt::WApplication::UpdateLock lock(LmsApp); - cover = CoverArt::Grabber::instance().getFromTrack(LmsApp->getDboSession(), *trackId, Image::Format::JPEG, *size); + cover = getServices().coverArtGrabber->getFromTrack(LmsApp->getDboSession(), *trackId, Image::Format::JPEG, *size); } } else if (releaseIdStr) @@ -93,7 +92,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // transactions are not thread safe { Wt::WApplication::UpdateLock lock(LmsApp); - cover = CoverArt::Grabber::instance().getFromRelease(LmsApp->getDboSession(), *releaseId, Image::Format::JPEG, *size); + cover = getServices().coverArtGrabber->getFromRelease(LmsApp->getDboSession(), *releaseId, Image::Format::JPEG, *size); } } else diff --git a/src/utils/Logger.cpp b/src/utils/Logger.cpp index babdf338..10ed9026 100644 --- a/src/utils/Logger.cpp +++ b/src/utils/Logger.cpp @@ -32,6 +32,7 @@ std::string getModuleName(Module mod) case Module::METADATA: return "METADATA"; case Module::REMOTE: return "REMOTE"; case Module::SERVICE: return "SERVICE"; + case Module::SIMILARITY: return "SIMILARITY"; case Module::TRANSCODE: return "TRANSCODE"; case Module::UI: return "UI"; } diff --git a/src/utils/Logger.hpp b/src/utils/Logger.hpp index dc2dd11f..837c14eb 100644 --- a/src/utils/Logger.hpp +++ b/src/utils/Logger.hpp @@ -44,6 +44,7 @@ enum class Module METADATA, REMOTE, SERVICE, + SIMILARITY, TRANSCODE, UI, }; diff --git a/src/utils/Utils.hpp b/src/utils/Utils.hpp index 287a62e4..f76bba2c 100644 --- a/src/utils/Utils.hpp +++ b/src/utils/Utils.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -63,3 +64,34 @@ boost::optional readAs(const std::string& str) std::string replaceInString(std::string str, const std::string& from, const std::string& to); +// warning: not efficient +template::value_type> +void uniqueAndSortedByOccurence(In first, In last, Out out) +{ + std::map occurencesMap; + + for (In it = first; it != last; ++it) + { + if (occurencesMap.find(*it) == occurencesMap.end()) + occurencesMap[*it] = 0; + + occurencesMap[*it]++; + } + + struct Item + { + U elem; + std::size_t count; + }; + + std::vector occurencesVector; + for (const auto& occurence : occurencesMap) + occurencesVector.emplace_back(Item{occurence.first, occurence.second}); + + std::sort(occurencesVector.begin(), occurencesVector.end(), [](const auto& a, const auto& b) { return a.count > b.count;}); + + for (const auto& occurence : occurencesVector) + *out++ = occurence.elem; +} + + diff --git a/tools/Makefile.am b/tools/Makefile.am index 19a2adce..c3c14142 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = metadata clusterer +SUBDIRS = feature-extractor metadata diff --git a/tools/clusterer/LmsClusterer.cpp b/tools/clusterer/LmsClusterer.cpp deleted file mode 100644 index 7d077d98..00000000 --- a/tools/clusterer/LmsClusterer.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "clusterer/SOM.hpp" -#include "clusterer/DataNormalizer.hpp" -#include "clusterer/Clusterer.hpp" -#include "database/DatabaseHandler.hpp" -#include "database/Track.hpp" -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "utils/Config.hpp" - -static std::vector features = -{ - "lowlevel.average_loudness", - "lowlevel.barkbands_flatness_db.mean", - "lowlevel.dissonance.mean", - "lowlevel.dynamic_complexity", - "lowlevel.hfc.mean", // GOOD - "lowlevel.melbands_crest.mean", - "lowlevel.melbands_kurtosis.mean", - "lowlevel.melbands_skewness.mean", - "lowlevel.melbands_spread.mean", - "lowlevel.pitch_salience.mean", - "lowlevel.pitch_salience.var", - "lowlevel.silence_rate_30dB.mean", - "lowlevel.silence_rate_60dB.mean", - "lowlevel.spectral_centroid.mean", - "lowlevel.spectral_complexity.mean", - "lowlevel.spectral_decrease.mean", - "lowlevel.spectral_energy.mean", - "lowlevel.spectral_energyband_high.mean", - "lowlevel.spectral_energyband_low.mean", - "lowlevel.spectral_energyband_middle_high.mean", - "lowlevel.spectral_energyband_middle_low.mean", - "lowlevel.spectral_entropy.mean", - "lowlevel.spectral_flux.mean", - "lowlevel.spectral_kurtosis.mean", - "lowlevel.spectral_rms.mean", - "lowlevel.spectral_skewness.mean", - "lowlevel.spectral_spread.mean", - "lowlevel.spectral_strongpeak.mean", - "lowlevel.zerocrossingrate.mean", - "rhythm.beats_loudness.mean", // BAD - "rhythm.bpm", - "tonal.chords_changes_rate", // OK -// "tonal.chords_number_rate", // BAD - "tonal.chords_strength.mean", // OK - "tonal.hpcp_entropy.mean", // GOOD -}; - -static size_t writeToFile(void *buffer, size_t size, size_t nmemb, void* ctx) -{ - std::ofstream& ofs = *reinterpret_cast(ctx); - - ofs.write(reinterpret_cast(buffer), size * nmemb); - - return size * nmemb; -} - -static void acousticBrainzGetLowLevel(const std::string& mbid, boost::filesystem::path output) -{ - std::string url = "http://acousticbrainz.org/api/v1/" + mbid + "/low-level"; - - std::cout << "GET " << url << std::endl; - - CURL *curl; - CURLcode res; - - curl = curl_easy_init(); - if (!curl) - { - return; - } - - std::ofstream ofs(output.string().c_str()); - if (!ofs) - { - curl_easy_cleanup(curl); - std::cerr << "Cannot open " << output.string() << " for writing purpose" << std::endl; - return; - } - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToFile); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ofs); - - res = curl_easy_perform(curl); - - if (res != CURLE_OK) - { - std::cerr << "perform failed: " << curl_easy_strerror(res) << std::endl; - } - - curl_easy_cleanup(curl); -} - -static boost::filesystem::path getLowLevelFeaturePath(const std::string& mbid) -{ - return boost::filesystem::path(Config::instance().getPath("working-dir") / "features" / mbid); -} - -std::vector getFeatures(const std::string& mbid) -{ - - std::vector res; - - try - { - boost::property_tree::ptree root; - - boost::property_tree::read_json(getLowLevelFeaturePath(mbid).string(), root); - - for (const auto& feature : features) - { - res.push_back(root.get(feature)); - } - } - catch (std::exception& e) - { - std::cerr << "Caught exception during processing " << mbid << std::endl; - } - - return res; -} - -int main(int argc, char *argv[]) -{ - try - { - boost::filesystem::path configFilePath = "/etc/lms.conf"; - - if (argc >= 2) - configFilePath = std::string(argv[1], 0, 256); - - Config::instance().setFile(configFilePath); - - Database::Handler::configureAuth(); - auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"); - Database::Handler db(*connectionPool); - - Wt::Dbo::Transaction transaction(db.getSession()); - - auto tracks = Database::Track::getAll(db.getSession()); - - std::vector, Database::IdType>> entries; - - std::cout << "Constructing input vectors..." << std::endl; - - for (auto track : tracks) - { - if (track->getMBID().empty()) - continue; - - auto path = getLowLevelFeaturePath(track->getMBID()); - if (!boost::filesystem::exists(path)) - acousticBrainzGetLowLevel(track->getMBID(), path); - - if (!boost::filesystem::exists(path)) - continue; - - std::pair, Database::IdType> entry; - - entry.first = getFeatures(track->getMBID()); - entry.second = track.id(); - - if (entry.first.size() == features.size()) - entries.push_back(std::move(entry)); - } - - std::cout << "Constructing input vectors... DONE" << std::endl; - - std::cout << "Clutering..." << std::endl; - Clusterer clusterer(entries, features.size(), 500); - - std::cout << "Clusterer :" << std::endl; - clusterer.dump(std::cout); - std::cout << std::endl; - - for (const auto& cluster : clusterer.getAllClusters()) - { - std::cout << "******************" << std::endl; - for (const auto& value : cluster) - { - auto track = Database::Track::getById(db.getSession(), value); - auto artist = track->getArtist(); - auto release = track->getRelease(); - - std::cout << "\t" << value << " - " << (artist ? artist->getName() : "") << " - " << (release ? release->getName() : "" ) << " - " << track->getName() << std::endl; - } - std::cout << std::endl; - } - } - catch( std::exception& e) - { - std::cerr << "Caught exception: " << e.what() << std::endl; - } - - return EXIT_SUCCESS; -} - diff --git a/tools/feature-extractor/LmsFeatureExtractor.cpp b/tools/feature-extractor/LmsFeatureExtractor.cpp new file mode 100644 index 00000000..aed394db --- /dev/null +++ b/tools/feature-extractor/LmsFeatureExtractor.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include + +#include + +#include "database/DatabaseHandler.hpp" +#include "database/Track.hpp" +#include "utils/Config.hpp" + +static size_t writeToOstream(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + std::ofstream& ofs = *reinterpret_cast(userdata); + ofs.write(ptr, size*nmemb); + return size*nmemb; +} + +static void acousticBrainzGetLowLevel(const std::string& mbid, boost::filesystem::path output) +{ + std::string url = "http://acousticbrainz.org/api/v1/" + mbid + "/low-level"; + + std::cout << "GET " << url << std::endl; + + CURL *curl; + CURLcode res; + + curl = curl_easy_init(); + if (!curl) + { + return; + } + + std::ofstream ofs(output.string().c_str()); + if (!ofs) + { + curl_easy_cleanup(curl); + std::cerr << "Cannot open " << output.string() << " for writing purpose" << std::endl; + return; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToOstream); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ofs); + + res = curl_easy_perform(curl); + + if (res != CURLE_OK) + { + std::cerr << "perform failed: " << curl_easy_strerror(res) << std::endl; + } + + curl_easy_cleanup(curl); +} + +static boost::filesystem::path getLowLevelFeaturePath(const std::string& mbid) +{ + return boost::filesystem::path(Config::instance().getPath("working-dir") / "features" / mbid); +} + +int main(int argc, char *argv[]) +{ + try + { + boost::filesystem::path configFilePath = "/etc/lms.conf"; + + if (argc >= 2) + configFilePath = std::string(argv[1], 0, 256); + + Config::instance().setFile(configFilePath); + + Database::Handler::configureAuth(); + auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"); + Database::Handler db(*connectionPool); + + Wt::Dbo::Transaction transaction(db.getSession()); + + auto tracks = Database::Track::getAll(db.getSession()); + + for (auto track : tracks) + { + if (track->getMBID().empty()) + continue; + + auto path = getLowLevelFeaturePath(track->getMBID()); + if (!boost::filesystem::exists(path)) + acousticBrainzGetLowLevel(track->getMBID(), path); + } + + } + catch( std::exception& e) + { + std::cerr << "Caught exception: " << e.what() << std::endl; + } + + return EXIT_SUCCESS; +} + diff --git a/tools/clusterer/Makefile.am b/tools/feature-extractor/Makefile.am similarity index 67% rename from tools/clusterer/Makefile.am rename to tools/feature-extractor/Makefile.am index 179ffc34..b886b16e 100644 --- a/tools/clusterer/Makefile.am +++ b/tools/feature-extractor/Makefile.am @@ -1,12 +1,11 @@ -bin_PROGRAMS = lms-clusterer +bin_PROGRAMS = lms-feature-extractor -lms_clusterer_SOURCES = \ - $(srcdir)/LmsClusterer.cpp \ - $(top_srcdir)/src/clusterer/DataNormalizer.cpp \ - $(top_srcdir)/src/clusterer/SOM.cpp \ +lms_feature_extractor_SOURCES = \ + $(srcdir)/LmsFeatureExtractor.cpp \ $(top_srcdir)/src/database/Artist.cpp \ $(top_srcdir)/src/database/Cluster.cpp \ $(top_srcdir)/src/database/DatabaseHandler.cpp \ + $(top_srcdir)/src/database/TrackFeature.cpp \ $(top_srcdir)/src/database/TrackList.cpp \ $(top_srcdir)/src/database/Release.cpp \ $(top_srcdir)/src/database/ScanSettings.cpp \ @@ -17,5 +16,5 @@ lms_clusterer_SOURCES = \ $(top_srcdir)/src/utils/Logger.cpp \ $(top_srcdir)/src/utils/Utils.cpp -lms_clusterer_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT +lms_feature_extractor_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT