From af97d6af4ddadaf1ac82253fce3840350fa5ce38 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 23 May 2016 20:06:13 +0200 Subject: [PATCH] [DB] Reworked feature extraction --- conf/lms.conf | 18 ++- src/Makefile.am | 5 +- src/config/Config.hpp | 1 + src/database/DatabaseFeatureExtractor.cpp | 82 ++++++++++++ src/database/DatabaseFeatureExtractor.hpp | 33 +++++ src/database/DatabaseHandler.cpp | 12 +- src/database/DatabaseUpdater.cpp | 126 +++++++++---------- src/database/DatabaseUpdater.hpp | 15 ++- src/database/MediaDirectory.cpp | 5 +- src/feature/FeatureExtractor.cpp | 32 +++-- src/feature/FeatureExtractor.hpp | 2 + src/feature/FeatureStore.cpp | 145 ++++++++++++++++++++++ src/feature/FeatureStore.hpp | 50 ++++++++ src/logger/Logger.cpp | 12 +- src/logger/Logger.hpp | 2 +- src/ui/audio/desktop/TableFilter.cpp | 9 +- 16 files changed, 444 insertions(+), 105 deletions(-) create mode 100644 src/database/DatabaseFeatureExtractor.cpp create mode 100644 src/database/DatabaseFeatureExtractor.hpp create mode 100644 src/feature/FeatureStore.cpp create mode 100644 src/feature/FeatureStore.hpp diff --git a/conf/lms.conf b/conf/lms.conf index 79adc916..bc42e092 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -2,10 +2,10 @@ # Path to the DB file. # Must have write privileges in order to create and modify the file -db-path = "/var/lms/lms.db"; +db-path = "/storage/emeric/lms/lms.dev.db"; # Listen port of the web server -listen-port = 5081; +listen-port = 5082; # Listen address of the web server listen-addr = "0.0.0.0"; @@ -22,3 +22,17 @@ approot = "/usr/share/lms/approot"; # Path to the wt_config.xml file wt-config = "/etc/wt/wt_config.xml"; + +# -- Settings to be moved in the web interface -- + +features-dir-path = "/var/lms/features/"; + +# Create tags from genres in metadata +# tag-genres = true; + +# Create tags from the high level data on AcousticBrainz +tag-highlevel-acousticbrainz = true; + +# Create similarity tags from the low level data on AcousticBrainz +tag-similarity-acousticbrainz = true; + diff --git a/src/Makefile.am b/src/Makefile.am index ecdd454f..2e541383 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -7,18 +7,21 @@ lms_SOURCES = \ $(srcdir)/config/Config.cpp \ $(srcdir)/cover/CoverArtGrabber.cpp \ $(srcdir)/database/Artist.cpp \ - $(srcdir)/database/DatabaseClassifier.cpp \ + $(srcdir)/database/DatabaseFeatureExtractor.cpp \ $(srcdir)/database/DatabaseHandler.cpp \ $(srcdir)/database/DatabaseUpdater.cpp \ $(srcdir)/database/MediaDirectory.cpp \ $(srcdir)/database/Playlist.cpp \ $(srcdir)/database/Release.cpp \ $(srcdir)/database/SearchFilter.cpp \ + $(srcdir)/database/Setting.cpp \ $(srcdir)/database/SqlQuery.cpp \ $(srcdir)/database/Track.cpp \ $(srcdir)/database/User.cpp \ $(srcdir)/database/Video.cpp \ + $(srcdir)/database/cluster/DatabaseHighLevelCluster.cpp \ $(srcdir)/feature/FeatureExtractor.cpp \ + $(srcdir)/feature/FeatureStore.cpp \ $(srcdir)/image/Image.cpp \ $(srcdir)/logger/Logger.cpp \ $(srcdir)/metadata/AvFormat.cpp \ diff --git a/src/config/Config.hpp b/src/config/Config.hpp index fef75058..afa0863d 100644 --- a/src/config/Config.hpp +++ b/src/config/Config.hpp @@ -22,6 +22,7 @@ #include #include +// Used to get config values from configuration files class Config { public: diff --git a/src/database/DatabaseFeatureExtractor.cpp b/src/database/DatabaseFeatureExtractor.cpp new file mode 100644 index 00000000..35574170 --- /dev/null +++ b/src/database/DatabaseFeatureExtractor.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 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 "config/Config.hpp" +#include "logger/Logger.hpp" + +#include "feature/FeatureExtractor.hpp" +#include "feature/FeatureStore.hpp" + +#include "DatabaseFeatureExtractor.hpp" + +namespace Database { + +static std::string getMBID(Track::id_type trackId) +{ + Wt::Dbo::Transaction transaction(UpdaterDboSession()); + Track::pointer track = Track::getById(UpdaterDboSession(), trackId); + return track->getMBID(); +} + +void +FeatureExtractor::processDatabaseUpdate(Updater::Stats stats) +{ + bool fetchHighLevel = Config::instance().getBool("tag-highlevel-acousticbrainz", false); + bool fetchLowLevel = Config::instance().getBool("tag-similarity-acousticbrainz", false); + + if (!fetchHighLevel && !fetchLowLevel) + { + LMS_LOG(DBUPDATER, INFO) << "No need to extract features"; + return; + } + + LMS_LOG(DBUPDATER, INFO) << "Processing tracks in order to extract features..."; + std::vector trackIds = Track::getAllIds(UpdaterDboSession()); + for (auto trackId : trackIds) + { + if (UpdaterQuitRequested()) + return; + + std::string mbid = getMBID(trackId); + if (mbid.empty()) + { + LMS_LOG(DBUPDATER, DEBUG) << "No MBID for track " << trackId << ", skipping"; + continue; + } + + if (fetchLowLevel && !Feature::Store::instance().exists(UpdaterDboSession(), trackId, "low_level")) + { + boost::property_tree::ptree feature; + if (::Feature::Extractor::getLowLevel(feature, mbid)) + Feature::Store::instance().set(UpdaterDboSession(), trackId, "low_level", feature); + } + + if (fetchHighLevel && !Feature::Store::instance().exists(UpdaterDboSession(), trackId, "high_level")) + { + boost::property_tree::ptree feature; + if (::Feature::Extractor::getHighLevel(feature, mbid)) + Feature::Store::instance().set(UpdaterDboSession(), trackId, "high_level", feature); + } + } + + LMS_LOG(DBUPDATER, INFO) << "Features have been extracted"; +} + +} // namespace Database + diff --git a/src/database/DatabaseFeatureExtractor.hpp b/src/database/DatabaseFeatureExtractor.hpp new file mode 100644 index 00000000..2a7c268c --- /dev/null +++ b/src/database/DatabaseFeatureExtractor.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 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/DatabaseUpdater.hpp" + +namespace Database { + +class FeatureExtractor +{ + public: + void processDatabaseUpdate(Updater::Stats stats); + +}; + +} // namespace Database diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index 9cf10f7f..e6caf88f 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -83,13 +83,12 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool) _session.mapClass("artist"); _session.mapClass("cluster"); _session.mapClass("track"); - _session.mapClass("feature"); _session.mapClass("playlist"); _session.mapClass("playlist_entry"); _session.mapClass("release"); _session.mapClass("video"); _session.mapClass("media_directory"); - _session.mapClass("media_directory_settings"); + _session.mapClass("setting"); _session.mapClass("user"); _session.mapClass("auth_info"); @@ -101,13 +100,10 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool) _session.createTables(); _session.execute("CREATE INDEX artist_name_idx ON artist(name)"); - _session.execute("CREATE INDEX cluster_name_idx ON cluster(name)"); - _session.execute("CREATE INDEX cluster_type_idx ON cluster(type)"); - _session.execute("CREATE INDEX cluster_name_type_idx ON cluster(name, type)"); _session.execute("CREATE INDEX release_name_idx ON release(name)"); - _session.execute("CREATE INDEX track_name_idx ON track(name)"); - _session.execute("CREATE INDEX feature_type_idx ON feature(type)"); - _session.execute("CREATE INDEX feature_track_type_idx ON feature(track_id,type)"); + _session.execute("CREATE INDEX track_artist_idx ON track(artist_id)"); + _session.execute("CREATE INDEX track_release_idx ON track(release_id)"); + _session.execute("CREATE INDEX cluster_type_idx ON cluster(type)"); } catch(std::exception& e) { LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); diff --git a/src/database/DatabaseUpdater.cpp b/src/database/DatabaseUpdater.cpp index f202284d..35b62a4e 100644 --- a/src/database/DatabaseUpdater.cpp +++ b/src/database/DatabaseUpdater.cpp @@ -162,7 +162,6 @@ Updater::stop(void) { _running = false; - // TODO cancel all jobs (timer, ...) _scheduleTimer.cancel(); _ioService.stop(); @@ -235,64 +234,67 @@ Updater::scheduleScan( boost::posix_time::ptime time) void Updater::process(boost::system::error_code err) { - if (!err) + if (err) + return; + + updateFileExtensions(); + + Stats stats; + + checkAudioFiles(stats); + checkVideoFiles(stats); + + std::vector rootDirectories; { - updateFileExtensions(); + Wt::Dbo::Transaction transaction(_db->getSession()); - Stats stats; - - checkAudioFiles(stats); - checkVideoFiles(stats); - - std::vector rootDirectories; - { - Wt::Dbo::Transaction transaction(_db->getSession()); - - for (MediaDirectory::pointer directory : MediaDirectory::getAll(_db->getSession())) - rootDirectories.push_back( RootDirectory( directory->getType(), directory->getPath() )); - } - - for (RootDirectory rootDirectory : rootDirectories) - { - if (!_running) - break; - - LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'..."; - processRootDirectory(rootDirectory, stats); - LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE"; - } - - if (_running) - checkDuplicatedAudioFiles(stats); - - LMS_LOG(DBUPDATER, INFO) << "Scan complete. Scanned = " << stats.nbScanned << ", Skipped = " << stats.nbSkipped << ", Changes = " << stats.nbChanges() << " (added = " << stats.nbAdded << ", nbRemoved = " << stats.nbRemoved << ", nbModified = " << stats.nbModified << "), Scan errors = " << stats.nbScanErrors << ", Not imported = " << stats.nbNotImported; - - // Update database stats - boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - { - Wt::Dbo::Transaction transaction(_db->getSession()); - - Database::MediaDirectorySettings::pointer settings = Database::MediaDirectorySettings::get(_db->getSession()); - - if (stats.nbChanges() > 0) - settings.modify()->setLastUpdate(now); - - // Save the last scan only if it has been completed - if (_running) - settings.modify()->setLastScan(now); - - // If the manual scan was required we can now set it to done - // Update only if the scan is complete! - if (settings->getManualScanRequested() && _running) - settings.modify()->setManualScanRequested(false); - - } - - scanComplete().emit(stats); - - if (_running) - processNextJob(); + for (MediaDirectory::pointer directory : MediaDirectory::getAll(_db->getSession())) + rootDirectories.push_back( RootDirectory( directory->getType(), directory->getPath() )); } + + for (RootDirectory rootDirectory : rootDirectories) + { + if (!_running) + break; + + LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'..."; + processRootDirectory(rootDirectory, stats); + LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE"; + } + + if (_running) + { + checkDuplicatedAudioFiles(stats); + + LMS_LOG(DBUPDATER, INFO) << "Processed all files, now calling listeners..."; + scanComplete().emit(stats); + } + + LMS_LOG(DBUPDATER, INFO) << "Scan complete. Scanned = " << stats.nbScanned << ", Skipped = " << stats.nbSkipped << ", Changes = " << stats.nbChanges() << " (added = " << stats.nbAdded << ", nbRemoved = " << stats.nbRemoved << ", nbModified = " << stats.nbModified << "), Scan errors = " << stats.nbScanErrors << ", Not imported = " << stats.nbNotImported; + + // Update database stats + boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); + { + Wt::Dbo::Transaction transaction(_db->getSession()); + + Database::MediaDirectorySettings::pointer settings = Database::MediaDirectorySettings::get(_db->getSession()); + + if (stats.nbChanges() > 0) + settings.modify()->setLastUpdate(now); + + // Save the last scan only if it has been completed + if (_running) + settings.modify()->setLastScan(now); + + // If the manual scan was required we can now set it to done + // Update only if the scan is complete! + if (settings->getManualScanRequested() && _running) + settings.modify()->setManualScanRequested(false); + + } + + if (_running) + processNextJob(); } void @@ -538,12 +540,9 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) { LMS_LOG(DBUPDATER, INFO) << "Updating '" << file << "'"; - // TODO Remove the songs from its clusters - // TODO Remove the features of this song - track.remove(); - track.flush(); - - track = Track::create(_db->getSession(), file); + // Remove the songs from its clusters + for (auto cluster : track->getClusters()) + cluster.remove(); stats.nbModified++; } @@ -614,8 +613,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) _sigTrackChanged.emit(true, track.id(), track->getMBID(), track->getPath()); } - - void +void Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats) { boost::system::error_code ec; diff --git a/src/database/DatabaseUpdater.hpp b/src/database/DatabaseUpdater.hpp index 08ae0498..3d032c4e 100644 --- a/src/database/DatabaseUpdater.hpp +++ b/src/database/DatabaseUpdater.hpp @@ -73,6 +73,9 @@ class Updater std::mutex& getMutex(void) { return _mutex; } + Database::Handler& getDb(void) { return *_db; } + + bool quitRequested(void) const { return !_running;} private: Updater(); @@ -133,8 +136,18 @@ class Updater MetaData::TagLibParser _metadataParser; - }; // class Updater +// Helper to get the updater session data +static inline Wt::Dbo::Session& UpdaterDboSession() +{ + return Updater::instance().getDb().getSession(); +} + +static inline bool UpdaterQuitRequested() +{ + return Updater::instance().quitRequested(); +} + } // Database diff --git a/src/database/MediaDirectory.cpp b/src/database/MediaDirectory.cpp index 75f25724..3bbf1ab2 100644 --- a/src/database/MediaDirectory.cpp +++ b/src/database/MediaDirectory.cpp @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -#include - #include "Types.hpp" static std::string pathsToString(const std::vector& paths) @@ -111,8 +109,7 @@ MediaDirectory::create(Wt::Dbo::Session& session, boost::filesystem::path p, Typ void MediaDirectory::eraseAll(Wt::Dbo::Session& session) { - std::vector dirs = getAll(session); - BOOST_FOREACH(MediaDirectory::pointer dir, dirs) + for (auto dir : getAll(session)) dir.remove(); } diff --git a/src/feature/FeatureExtractor.cpp b/src/feature/FeatureExtractor.cpp index 23f54ff5..914757af 100644 --- a/src/feature/FeatureExtractor.cpp +++ b/src/feature/FeatureExtractor.cpp @@ -42,7 +42,7 @@ Extractor::init(void) if (extractorPath.empty()) { - LMS_LOG(CLASSIFICATION, ERROR) << "Failed to find path to " << execName; + LMS_LOG(FEATURE, ERROR) << "Failed to find path to " << execName; return false; } @@ -71,17 +71,17 @@ static bool fetchJSONData(boost::property_tree::ptree& pt, std::string url) } catch( curlpp::RuntimeError &e ) { - LMS_LOG(CLASSIFICATION, ERROR) << "curlpp error: " << e.what(); + LMS_LOG(FEATURE, ERROR) << "curlpp error: " << e.what(); return false; } catch( curlpp::LogicError &e ) { - LMS_LOG(CLASSIFICATION, ERROR) << "curlpp error: " << e.what(); + LMS_LOG(FEATURE, ERROR) << "curlpp error: " << e.what(); return false; } catch ( boost::property_tree::ptree_error& e) { - LMS_LOG(CLASSIFICATION, ERROR) << "JSON paring failed: " << e.what(); + LMS_LOG(FEATURE, ERROR) << "JSON paring failed: " << e.what(); return false; } @@ -92,7 +92,7 @@ static bool fetchJSONData(boost::property_tree::ptree& pt, std::string url) bool Extractor::getLowLevel(boost::property_tree::ptree& pt, std::string mbid) { - LMS_LOG(CLASSIFICATION, DEBUG) << "Trying to fetch low level metadata for track '" << mbid << "' on AcousticBrainz"; + LMS_LOG(FEATURE, DEBUG) << "Trying to fetch low level metadata for track '" << mbid << "' on AcousticBrainz"; boost::property_tree::ptree res; if (!fetchJSONData(res, "https://acousticbrainz.org/" + mbid + "/low-level")) @@ -101,14 +101,14 @@ Extractor::getLowLevel(boost::property_tree::ptree& pt, std::string mbid) auto message = res.get_child_optional("message"); if (message) { - LMS_LOG(CLASSIFICATION, ERROR) << "Cannot get data on AcousticBrainz: " << message->data(); + LMS_LOG(FEATURE, ERROR) << "Track '" << mbid << "': cannot get data on AcousticBrainz: " << message->data(); return false; } auto lowlevel = res.get_child_optional("lowlevel"); if (!lowlevel) { - LMS_LOG(CLASSIFICATION, ERROR) << "Low level data not found!"; + LMS_LOG(FEATURE, ERROR) << "Track '" << mbid << "': low level data not found!"; return false; } @@ -122,7 +122,11 @@ Extractor::getLowLevel(boost::property_tree::ptree& pt, std::string mbid) bool Extractor::getHighLevel(boost::property_tree::ptree& pt, std::string mbid) { - LMS_LOG(CLASSIFICATION, DEBUG) << "Trying to fetch high level metadata for track '" << mbid << "' on AcousticBrainz"; + LMS_LOG(FEATURE, DEBUG) << "Trying to fetch high level metadata for track '" << mbid << "' on AcousticBrainz"; + + // TODO check MBID + if (mbid.empty()) + return false; boost::property_tree::ptree res; if (!fetchJSONData(res, "https://acousticbrainz.org/" + mbid + "/high-level")) @@ -131,17 +135,19 @@ Extractor::getHighLevel(boost::property_tree::ptree& pt, std::string mbid) auto message = res.get_child_optional("message"); if (message) { - LMS_LOG(CLASSIFICATION, ERROR) << "Cannot get data on AcousticBrainz: " << message->data(); + LMS_LOG(FEATURE, ERROR) << "Track '" << mbid << "': cannot get data on AcousticBrainz: " << message->data(); return false; } auto lowlevel = res.get_child_optional("highlevel"); if (!lowlevel) { - LMS_LOG(CLASSIFICATION, ERROR) << "High level data not found!"; + LMS_LOG(FEATURE, ERROR) << "Track '" << mbid << "': high level data not found!"; return false; } + res.erase("metadata"); + pt = res; return true; @@ -151,7 +157,7 @@ Extractor::getHighLevel(boost::property_tree::ptree& pt, std::string mbid) bool Extractor::getLowLevel(boost::property_tree::ptree& pt, boost::filesystem::path path) { - LMS_LOG(CLASSIFICATION, DEBUG) << "Extracting low level data from '" << path << "'"; + LMS_LOG(FEATURE, DEBUG) << "Extracting low level data from '" << path << "'"; if (extractorPath.empty()) return false; @@ -168,7 +174,7 @@ Extractor::getLowLevel(boost::property_tree::ptree& pt, boost::filesystem::path in.open(extractorPath.string(), args); if (!in.is_open()) { - LMS_LOG(CLASSIFICATION, ERROR) << "Exec failed!"; + LMS_LOG(FEATURE, ERROR) << "Exec failed!"; return false; } @@ -198,7 +204,7 @@ Extractor::getLowLevel(boost::property_tree::ptree& pt, boost::filesystem::path } catch ( boost::property_tree::ptree_error& e) { - LMS_LOG(CLASSIFICATION, ERROR) << "JSON paring failed: " << e.what(); + LMS_LOG(FEATURE, ERROR) << "JSON parsing failed: " << e.what(); return false; } diff --git a/src/feature/FeatureExtractor.hpp b/src/feature/FeatureExtractor.hpp index ed7169c2..7696c968 100644 --- a/src/feature/FeatureExtractor.hpp +++ b/src/feature/FeatureExtractor.hpp @@ -25,6 +25,8 @@ namespace Feature { +typedef boost::property_tree::ptree Type; + class Extractor { public: diff --git a/src/feature/FeatureStore.cpp b/src/feature/FeatureStore.cpp new file mode 100644 index 00000000..e5fff4dc --- /dev/null +++ b/src/feature/FeatureStore.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2016 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 + +#include "logger/Logger.hpp" +#include "config/Config.hpp" + +#include "FeatureStore.hpp" + +namespace Feature { + +Store::Store() +{ +} + +Store& +Store::instance(void) +{ + static Store instance; + return instance; +} + +void +Store::reload(void) +{ + _storePath = Config::instance().getString("features-dir-path", ""); + + if (!boost::filesystem::is_directory(_storePath)) + { + LMS_LOG(DBUPDATER, ERROR) << "Feature directory '" << _storePath << "' not valid!"; + throw std::runtime_error("Invalid feature directory '" + _storePath.string()); + } +} + +boost::filesystem::path getPath(boost::filesystem::path root, std::string mbid, std::string type) +{ + return root / (mbid + "_" + type); +} + +bool +Store::exists(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type) +{ + if (_storePath.empty()) + reload(); + + Wt::Dbo::Transaction transaction(session); + + auto track = Database::Track::getById(session, trackId); + std::string mbid = track->getMBID(); + + transaction.commit(); + + if (mbid.empty()) + return false; + + boost::filesystem::path path = getPath(_storePath, mbid, type); + return boost::filesystem::exists(path); +} + +bool +Store::get(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type, Type& feature) +{ + if (_storePath.empty()) + reload(); + + Wt::Dbo::Transaction transaction(session); + + auto track = Database::Track::getById(session, trackId); + std::string mbid = track->getMBID(); + + transaction.commit(); + + if (mbid.empty()) + return false; + + boost::filesystem::path path = getPath(_storePath, mbid, type); + if (!boost::filesystem::exists(path)) + return false; + + try + { + std::ifstream iss(path.string().c_str(), std::ios::in); + boost::property_tree::json_parser::read_json(iss, feature); + } + catch (boost::property_tree::ptree_error& e) + { + LMS_LOG(FEATURE, ERROR) << "JSON parsing failed: " << e.what(); + return false; + } + + return true; +} + + +bool +Store::set(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type, const Type& feature) +{ + if (_storePath.empty()) + reload(); + + Wt::Dbo::Transaction transaction(session); + + auto track = Database::Track::getById(session, trackId); + std::string mbid = track->getMBID(); + + transaction.commit(); + + if (mbid.empty()) + return false; + + boost::filesystem::path path = getPath(_storePath, mbid, type); + + try + { + std::ofstream oss(path.string().c_str(), std::ios::out); + boost::property_tree::json_parser::write_json(oss, feature); + } + catch (boost::property_tree::ptree_error& e) + { + LMS_LOG(FEATURE, ERROR) << "JSON writing failed: " << e.what(); + return false; + } + + return true; +} + +} // namespace CoverArt + diff --git a/src/feature/FeatureStore.hpp b/src/feature/FeatureStore.hpp new file mode 100644 index 00000000..7716d9d7 --- /dev/null +++ b/src/feature/FeatureStore.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 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 "FeatureExtractor.hpp" + +namespace Feature { + +class Store +{ + public: + Store(const Store&) = delete; + Store& operator=(const Store&) = delete; + + static Store& instance(); + + bool exists(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type); + bool get(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type, Type& feature); + + bool set(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::string type, const Type& feature); + + void reload(); + private: + Store(); + + boost::filesystem::path _storePath; +}; + +} // namespace CoverArt + diff --git a/src/logger/Logger.cpp b/src/logger/Logger.cpp index aa02485c..babdf338 100644 --- a/src/logger/Logger.cpp +++ b/src/logger/Logger.cpp @@ -26,13 +26,13 @@ std::string getModuleName(Module mod) case Module::AV: return "AV"; case Module::COVER: return "COVER"; case Module::DB: return "DB"; - case Module::CLASSIFICATION: return "CLASSIFICATION"; - case Module::DBUPDATER: return "DB UPDATER"; + case Module::DBUPDATER: return "DB UPDATER"; + case Module::FEATURE: return "FEATURE"; case Module::MAIN: return "MAIN"; - case Module::METADATA: return "METADATA"; - case Module::REMOTE: return "REMOTE"; - case Module::SERVICE: return "SERVICE"; - case Module::TRANSCODE: return "TRANSCODE"; + case Module::METADATA: return "METADATA"; + case Module::REMOTE: return "REMOTE"; + case Module::SERVICE: return "SERVICE"; + case Module::TRANSCODE: return "TRANSCODE"; case Module::UI: return "UI"; } return ""; diff --git a/src/logger/Logger.hpp b/src/logger/Logger.hpp index 6c5b822d..b44a7d64 100644 --- a/src/logger/Logger.hpp +++ b/src/logger/Logger.hpp @@ -38,10 +38,10 @@ enum class Severity enum class Module { AV, - CLASSIFICATION, COVER, DB, DBUPDATER, + FEATURE, MAIN, METADATA, REMOTE, diff --git a/src/ui/audio/desktop/TableFilter.cpp b/src/ui/audio/desktop/TableFilter.cpp index 9f2677da..3ef75edd 100644 --- a/src/ui/audio/desktop/TableFilter.cpp +++ b/src/ui/audio/desktop/TableFilter.cpp @@ -33,7 +33,7 @@ using namespace Database; TableFilterCluster::TableFilterCluster(Wt::WContainerWidget* parent) : Wt::WTableView( parent ), Filter() { - const std::vector columnNames = {"Type", "Name", "Tracks"}; + const std::vector columnNames = {"Tag", "Tracks"}; SearchFilter filter; @@ -44,8 +44,7 @@ TableFilterCluster::TableFilterCluster(Wt::WContainerWidget* parent) this->setAlternatingRowColors(true); this->setModel(&_queryModel); - this->setColumnWidth(1, 120); - this->setColumnWidth(2, 80); + this->setColumnWidth(1, 80); this->selectionChanged().connect(this, &TableFilterCluster::emitUpdate); @@ -77,9 +76,9 @@ TableFilterCluster::TableFilterCluster(Wt::WContainerWidget* parent) void TableFilterCluster::layoutSizeChanged (int width, int height) { - std::size_t otherColumnSizes = this->columnWidth(1).toPixels() + this->columnWidth(2).toPixels(); + std::size_t trackColumnSize = this->columnWidth(1).toPixels(); // Set the remaining size for the name column - this->setColumnWidth(0, width - otherColumnSizes - (7 * 3) - 2); + this->setColumnWidth(0, width - trackColumnSize - (7 * 2) - 2); } // Set constraints on this filter