diff --git a/TODO b/TODO index 343ebbcc..51a0ca71 100644 --- a/TODO +++ b/TODO @@ -1,19 +1,12 @@ -[ServiceManager] -- Rework the whole start/stop/try/cach/thread/interrupts things -- Use our own WIOService - -[Services] -- consider adding support for external web servers (new FCGI service?) - -[Users] -- Handle login lifetime? -- Limit multi login from UI and remote interfaces (limit per interface is acceptable) +[Book] +- Make the feature [Cover] - Scaling: find something more "reliable" than GIL and its custom extensions (adobe work, io_new)? - Handle several file formats (not only jpg) - Handle preferred cover file names +- Implement a cache and a grabber from some web service (mandatory for artists) [Database] - Optim, use SQL query to get the "genre" orphans @@ -24,8 +17,22 @@ [Metadata] - OGG metadata -> properly handle metadata nested in the audio stream +[Playlist] +- Make public playlists so that users can see what other people are listening to + +[Users] +- Handle login lifetime? +- Limit multi login from UI and remote interfaces (limit per interface is acceptable) + +[ServiceManager] +- Rework the whole start/stop/try/cach/thread/interrupts things +- Use our own WIOService + +[Services] +- consider adding support for external web servers (new FCGI service?) + [Transcode] -- some early end of playback spotted on flac files. Spotted on old firefox versions? +- some early end of playback spotted on files. Maybe this is because the stderr of the forked process is not properly closed? [UI] - handle internationalization @@ -44,9 +51,10 @@ [Audio] - Style eveything nicely... - - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478) - - TrackView : handle durations > 1 hour - - TrackView : Reselect the current selected item when displaying the updated search results + - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) + - TrackView: handle durations > 1 hour + - TrackView: Reselect the current selected item when displaying the updated search results + - TrackView: display the total duration of the track query - Add covers in the release filter? - Add a download button to get the current playlist in a streamed zip file - Add a upload button to upload media files in a dedicated directory @@ -65,7 +73,7 @@ - Implement partial text search options in the GetXXX messages [REST API] -- Make another dedicated REST API. Maybe use the SubSonic API? +- Make another dedicated REST API. Maybe use the SubSonic API or Ampache API? [Logs] - Capture transcoder log output to get more information on errors diff --git a/conf/lms.conf.sample b/conf/lms.conf.sample index 9174d9d7..01ffec79 100644 --- a/conf/lms.conf.sample +++ b/conf/lms.conf.sample @@ -2,9 +2,14 @@ main = { logger = { - file = "/var/lms/lms.log" # comment to disable file logging - console = true; - level = 7; + level = 7; # level common for all loggers + file = { + enable = true; + path = "/var/lms/lms.log"; # comment to disable file logging + } + console = { + enable = true; + } } database = { @@ -16,15 +21,13 @@ main = { cover = { file_extensions = "jpg jpeg"; - file_max_size = 500000; + file_max_size = 5000000; file_preferred_names = "cover front"; } } ui = { - enable = true; - resources = { docroot = "/var/lms/docroot" approot = "/var/lms/approot" @@ -44,8 +47,6 @@ ui = { } remote = { - enable = true; - nb-threads = 1; listen-endpoint = { diff --git a/include/.empty b/include/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/src/lms-api/proto/auth.proto b/lms-api/proto/auth.proto similarity index 98% rename from src/lms-api/proto/auth.proto rename to lms-api/proto/auth.proto index b054ef06..1ad02a48 100644 --- a/src/lms-api/proto/auth.proto +++ b/lms-api/proto/auth.proto @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -import "common.proto"; - package LmsAPI; message AuthRequest diff --git a/src/lms-api/proto/collection.proto b/lms-api/proto/collection.proto similarity index 99% rename from src/lms-api/proto/collection.proto rename to lms-api/proto/collection.proto index 8037ab02..8666a98b 100644 --- a/src/lms-api/proto/collection.proto +++ b/lms-api/proto/collection.proto @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -import "common.proto"; - package LmsAPI; message AudioCollectionRequest diff --git a/src/lms-api/proto/media.proto b/lms-api/proto/media.proto similarity index 99% rename from src/lms-api/proto/media.proto rename to lms-api/proto/media.proto index 5dae7257..20af5d02 100644 --- a/src/lms-api/proto/media.proto +++ b/lms-api/proto/media.proto @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -import "common.proto"; - package LmsAPI; message MediaRequest diff --git a/src/lms-api/proto/messages.proto b/lms-api/proto/messages.proto similarity index 98% rename from src/lms-api/proto/messages.proto rename to lms-api/proto/messages.proto index 7ba930c3..a6eb280b 100644 --- a/src/lms-api/proto/messages.proto +++ b/lms-api/proto/messages.proto @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -import "common.proto"; - import "auth.proto"; import "collection.proto"; import "media.proto"; diff --git a/src/Makefile.am b/src/Makefile.am index e537e5c2..e60d4e1c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -81,8 +81,6 @@ nodist_lms_SOURCES = \ $(builddir)/auth.pb.h \ $(builddir)/collection.pb.cc \ $(builddir)/collection.pb.h \ - $(builddir)/common.pb.cc \ - $(builddir)/common.pb.h \ $(builddir)/media.pb.cc \ $(builddir)/media.pb.h \ $(builddir)/messages.pb.cc \ @@ -93,8 +91,6 @@ BUILT_SOURCES = \ $(builddir)/auth.pb.h \ $(builddir)/collection.pb.cc \ $(builddir)/collection.pb.h \ - $(builddir)/common.pb.cc \ - $(builddir)/common.pb.h \ $(builddir)/media.pb.cc \ $(builddir)/media.pb.h \ $(builddir)/messages.pb.cc \ @@ -105,15 +101,13 @@ MOSTLYCLEANFILES = \ $(builddir)/auth.pb.h \ $(builddir)/collection.pb.cc \ $(builddir)/collection.pb.h \ - $(builddir)/common.pb.cc \ - $(builddir)/common.pb.h \ $(builddir)/media.pb.cc \ $(builddir)/media.pb.h \ $(builddir)/messages.pb.cc \ $(builddir)/messages.pb.h -%.pb.cc %.pb.h: $(srcdir)/lms-api/proto/%.proto - $(PROTOC) --proto_path=$(srcdir)/lms-api/proto/ --cpp_out=$(builddir)/ $^ +%.pb.cc %.pb.h: $(top_srcdir)/lms-api/proto/%.proto + $(PROTOC) --proto_path=$(top_srcdir)/lms-api/proto/ --cpp_out=$(builddir)/ $^ endif diff --git a/src/config/ConfigReader.cpp b/src/config/ConfigReader.cpp index 5e351bc8..90c093de 100644 --- a/src/config/ConfigReader.cpp +++ b/src/config/ConfigReader.cpp @@ -23,89 +23,52 @@ namespace { - void splitStrings(const std::string& source, std::vector& res) - { - std::istringstream oss(source); - - std::string str; - while(oss >> str) - res.push_back(str); - } - } -ConfigReader::ConfigReader(boost::filesystem::path p) +ConfigReader::ConfigReader() +: _config (nullptr) { - _config.readFile(p.string().c_str()); +} + +ConfigReader& +ConfigReader::instance() +{ + static ConfigReader instance; + return instance; } void -ConfigReader::getLoggerConfig(Logger::Config& config) +ConfigReader::setFile(boost::filesystem::path p) { - config.enableFileLogging = _config.lookupValue("main.logger.file", config.logPath); - config.enableConsoleLogging = _config.lookup("main.logger.console"); - config.minSeverity = static_cast((int)_config.lookup("main.logger.level")); + if (_config != nullptr) + delete _config; + + _config = new libconfig::Config(); + + _config->readFile(p.string().c_str()); } -void -ConfigReader::getCoverGrabberConfig(CoverArt::Grabber::Config& config) +std::string +ConfigReader::getString(std::string setting) { - std::string extensions = _config.lookup("main.cover.file_extensions"); - config.maxFileSize = static_cast(_config.lookup("main.cover.file_max_size")); - std::string filenames = _config.lookup("main.cover.file_preferred_names"); - - splitStrings(extensions, config.fileExtensions); - splitStrings(filenames, config.preferredFileNames); + return _config->lookup(setting); } -void -ConfigReader::getUserInterfaceConfig(Service::UserInterfaceService::Config& config) +unsigned long +ConfigReader::getULong(std::string setting) { - - config.enable = _config.lookup("ui.enable"); - if (!config.enable) - return; - - config.docRootPath = _config.lookup("ui.resources.docroot"); - config.appRootPath = _config.lookup("ui.resources.approot"); - config.httpsPort = static_cast(_config.lookup("ui.listen-endpoint.port")); - config.httpsAddress = boost::asio::ip::address::from_string((const char*)_config.lookup("ui.listen-endpoint.addr")); - config.sslCertificatePath = _config.lookup("ui.ssl-crypto.cert"); - config.sslPrivateKeyPath = _config.lookup("ui.ssl-crypto.key"); - config.sslTempDhPath = _config.lookup("ui.ssl-crypto.dh"); - - config.dbPath = _config.lookup("main.database.path"); + return static_cast(_config->lookup(setting)); } -#if defined HAVE_LMSAPI -void -ConfigReader::getLmsAPIConfig(Service::LmsAPIService::Config& config) +long +ConfigReader::getLong(std::string setting) { - config.enable = _config.lookup("remote.enable"); - if (!config.enable) - return; - - config.port = static_cast(_config.lookup("remote.listen-endpoint.port")); - config.address = boost::asio::ip::address::from_string((const char*)_config.lookup("remote.listen-endpoint.addr")); - config.sslCertificatePath = _config.lookup("remote.ssl-crypto.cert"); - config.sslPrivateKeyPath = _config.lookup("remote.ssl-crypto.key"); - config.sslTempDhPath = _config.lookup("remote.ssl-crypto.dh"); - - config.dbPath = _config.lookup("main.database.path"); -} -#endif - -void -ConfigReader::getDatabaseUpdateConfig(Service::DatabaseUpdateService::Config& config) -{ - config.enable = true; - - config.dbPath = _config.lookup("main.database.path"); - - std::string audioExtensions = _config.lookup("main.database.audio_extensions"); - std::string videoExtensions = _config.lookup("main.database.video_extensions"); - - splitStrings(audioExtensions, config.audioExtensions); - splitStrings(videoExtensions, config.videoExtensions); + return _config->lookup(setting); +} + +bool +ConfigReader::getBool(std::string setting) +{ + return _config->lookup(setting); } diff --git a/src/config/ConfigReader.hpp b/src/config/ConfigReader.hpp index 6abc5039..6f68156c 100644 --- a/src/config/ConfigReader.hpp +++ b/src/config/ConfigReader.hpp @@ -23,40 +23,27 @@ #include #include -#include "config/config.h" - -#include "cover/CoverArtGrabber.hpp" -#include "logger/Logger.hpp" - -#include "service/UserInterfaceService.hpp" -#include "service/DatabaseUpdateService.hpp" -#if defined HAVE_LMSAPI -#include "service/LmsAPIServerService.hpp" -#endif - class ConfigReader { public: - ConfigReader(boost::filesystem::path p); + ConfigReader(const ConfigReader&) = delete; + ConfigReader& operator=(const ConfigReader&) = delete; - // Logger configuration - void getLoggerConfig(Logger::Config& config); + static ConfigReader& instance(); - // Covers - void getCoverGrabberConfig(CoverArt::Grabber::Config& config); + void setFile(boost::filesystem::path p); - // Service configurations - void getUserInterfaceConfig(Service::UserInterfaceService::Config& config); - void getDatabaseUpdateConfig(Service::DatabaseUpdateService::Config& config); - -#if defined HAVE_LMSAPI - void getLmsAPIConfig(Service::LmsAPIService::Config& config); -#endif + std::string getString(std::string setting); + unsigned long getULong(std::string setting); + long getLong(std::string setting); + bool getBool(std::string setting); private: - libconfig::Config _config; + ConfigReader(); + + libconfig::Config *_config; }; #endif diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 9194330a..695ddc3e 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -17,10 +17,8 @@ * along with LMS. If not, see . */ - -#include - #include "logger/Logger.hpp" +#include "config/ConfigReader.hpp" #include "av/InputFormatContext.hpp" #include "CoverArtGrabber.hpp" @@ -28,6 +26,18 @@ namespace { +std::vector splitStrings(const std::string& source) +{ + std::vector res; + std::istringstream oss(source); + + std::string str; + while(oss >> str) + res.push_back(str); + + return res; +} + bool isFileSupported(const boost::filesystem::path& file, const std::vector extensions) { @@ -58,12 +68,12 @@ Grabber::instance() } void -Grabber::init(const Config& config) +Grabber::init() { - for (auto extension : config.fileExtensions) + for (const std::string& extension : splitStrings( ConfigReader::instance().getString("main.cover.file_extensions"))) _fileExtensions.push_back("." + extension); - _maxFileSize = config.maxFileSize; + _maxFileSize = ConfigReader::instance().getULong("main.cover.file_max_size"); } std::vector @@ -75,7 +85,7 @@ Grabber::getFromInputFormatContext(const Av::InputFormatContext& input, std::siz { std::vector pictures = input.getPictures(nbMaxCovers); - BOOST_FOREACH(const Av::Picture& picture, pictures) + for (Av::Picture& picture : pictures) res.push_back( CoverArt(picture.mimeType, picture.data) ); } diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index b07bbb56..f2dbb33e 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -38,14 +38,7 @@ class Grabber static Grabber& instance(); - struct Config - { - std::vector fileExtensions; - std::size_t maxFileSize; - std::vector preferredFileNames; - }; - - void init(const Config& config); + void init(); std::vector getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const; std::vector getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; diff --git a/src/database-updater/DatabaseUpdater.cpp b/src/database-updater/DatabaseUpdater.cpp index dd4d0683..2239d96c 100644 --- a/src/database-updater/DatabaseUpdater.cpp +++ b/src/database-updater/DatabaseUpdater.cpp @@ -493,6 +493,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); track.modify()->setDuration( boost::any_cast(items[MetaData::Type::Duration]) ); + track.modify()->setAddedTime( boost::posix_time::second_clock::local_time() ); { std::string trackGenreList; @@ -630,19 +631,15 @@ void Updater::checkAudioFiles( Stats& stats ) { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking audio files..."; + LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Checking audio files..."; Wt::Dbo::Transaction transaction(_db.getSession()); std::vector rootDirs = getRootDirectoriesByType(_db.getSession(), Database::MediaDirectory::Audio); LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking tracks..."; - typedef Wt::Dbo::collection< Wt::Dbo::ptr > Tracks; - Tracks tracks = Track::getAll(_db.getSession()); - - for (Tracks::iterator it = tracks.begin(); it != tracks.end(); ++it) + auto tracks = Track::getAll(_db.getSession()); + for (auto track : tracks) { - Track::pointer track = (*it); - if (!checkFile(track->getPath(), rootDirs, _audioExtensions)) { track.remove(); @@ -651,20 +648,34 @@ Updater::checkAudioFiles( Stats& stats ) } // Now process orphan Genre (no track) -/* LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking Genres..."; - typedef Wt::Dbo::collection< Wt::Dbo::ptr > Genres; - Genres genres = Genre::getAll(_db.getSession()); - - for (Genres::iterator it = genres.begin(); it != genres.end(); ++it) + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking Genres..."; + auto genres = Genre::getAll(_db.getSession()); + for (auto genre : genres) { - Genre::pointer genre = (*it); - if (genre->getTracks().size() == 0) + { + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan genre '" << genre->getName() << "'"; genre.remove(); + } } -*/ - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Check audio files done!"; + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking artists..."; + auto artists = Artist::getAllOrphans(_db.getSession()); + for (auto artist : artists) + { + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; + artist.remove(); + } + + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking releases..."; + auto releases = Release::getAllOrphans(_db.getSession()); + for (auto release : releases) + { + LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan release '" << release->getName() << "'"; + release.remove(); + } + + LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Check audio files done!"; } void diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp index ffff62b7..f130c5a5 100644 --- a/src/database/Artist.cpp +++ b/src/database/Artist.cpp @@ -84,6 +84,18 @@ Artist::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, return std::vector(res.begin(), res.end()); } +std::vector > +Artist::getReleases() const +{ + assert(self()); + assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); + assert(session()); + + Wt::Dbo::collection< Wt::Dbo::ptr > res = session()->query >("SELECT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id").where("a.id = ?").bind(id()); + + return std::vector< Wt::Dbo::ptr > (res.begin(), res.end()); +} + Wt::Dbo::Query Artist::getQuery(Wt::Dbo::Session& session, SearchFilter filter) { diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp index 8c59ac07..e73bf58d 100644 --- a/src/database/Artist.hpp +++ b/src/database/Artist.hpp @@ -33,8 +33,9 @@ namespace Database class Track; class Genre; +class Release; -class Artist +class Artist : public Wt::Dbo::Dbo { public: @@ -58,6 +59,9 @@ class Artist std::string getName(void) const { return _name; } std::string getMBID(void) const { return _MBID; } + // Get the releases that have at least one track for this artist + std::vector > getReleases() const; + void setMBID(std::string mbid) { _MBID = mbid; } // Create diff --git a/src/database/Release.cpp b/src/database/Release.cpp index d2bd4f72..5ef5415a 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -71,7 +71,7 @@ Release::getAll(Wt::Dbo::Session& session, int offset, int size) std::vector Release::getAllOrphans(Wt::Dbo::Session& session) { - Wt::Dbo::collection res = session.query< Wt::Dbo::ptr >("select a from artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL"); + Wt::Dbo::collection res = session.query< Wt::Dbo::ptr >("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL"); return std::vector(res.begin(), res.end()); } @@ -129,4 +129,16 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, return std::vector(res.begin(), res.end()); } +std::vector< Wt::Dbo::ptr > +Release::getArtists() const +{ + assert(self()); + assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); + assert(session()); + + Wt::Dbo::collection< Wt::Dbo::ptr > res = session()->query >("SELECT a FROM artist a INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.release_id = r.id").where("r.id = ?").bind(id()); + + return std::vector< Wt::Dbo::ptr > (res.begin(), res.end()); +} + } // namespace Database diff --git a/src/database/Release.hpp b/src/database/Release.hpp index 479870a7..0bb779d0 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -29,8 +29,9 @@ namespace Database { class Track; +class Release; -class Release +class Release : public Wt::Dbo::Dbo { public: @@ -64,6 +65,8 @@ class Release std::string getMBID() const { return _MBID; } bool isNone(void) const; boost::posix_time::time_duration getDuration(void) const; + std::vector > getArtists() const; // Get the artists of this release + std::vector > getTracks() const; // Get the tracks of this release void setMBID(std::string mbid) { _MBID = mbid; } diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 850e72c6..7ad69914 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -113,6 +113,20 @@ Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter) return query; } +Track::StatsQueryResult +Track::getStats(Wt::Dbo::Session& session, SearchFilter filter) +{ + SqlQuery sqlQuery = generatePartialQuery(filter); + + Wt::Dbo::Query query = session.query( "SELECT COUNT(DISTINCT t.id), SUM(t.duration) FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get()); + + for (const std::string& bindArg : sqlQuery.where().getBindArgs()) + query.bind(bindArg); + + return query; +} + + std::vector Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) { @@ -152,11 +166,10 @@ Genre::Genre(const std::string& name) { } -std::vector -Genre::getAll(Wt::Dbo::Session& session, int offset, int size) +Wt::Dbo::collection +Genre::getAll(Wt::Dbo::Session& session) { - Wt::Dbo::collection res = session.find().offset(offset).limit(size); - return std::vector(res.begin(), res.end()); + return session.find(); } Genre::pointer diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 3d0e7fe5..14099ce7 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -52,7 +52,7 @@ class Genre static pointer getByName(Wt::Dbo::Session& session, const std::string& name); static pointer getNone(Wt::Dbo::Session& session); static std::vector getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); - static std::vector getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1); + static Wt::Dbo::collection getAll(Wt::Dbo::Session& session); // MVC models for the user interface // Genre ID, name, track count @@ -105,10 +105,8 @@ class Track static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p); static pointer getById(Wt::Dbo::Session& session, id_type id); static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID); - static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session); - - // Used for remote static std::vector getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); + static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session); // Utility fonctions // MVC models for the user interface @@ -127,6 +125,13 @@ class Track static Wt::Dbo::Query< UIQueryResult > getUIQuery(Wt::Dbo::Session& session, SearchFilter filter); static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector& columnNames = std::vector()); + // Stats for a given search filter + typedef boost::tuple< + int, // Total tracks + boost::posix_time::time_duration // Total duration + > StatsQueryResult; + static StatsQueryResult getStats(Wt::Dbo::Session& session, SearchFilter filter); + // Create utility static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p); diff --git a/src/lms-api/proto/common.proto b/src/lms-api/proto/common.proto deleted file mode 100644 index 3042d84d..00000000 --- a/src/lms-api/proto/common.proto +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -package LmsAPI; - -message Error -{ - required bool error = 1; - optional string message = 2; -} - diff --git a/src/logger/Logger.cpp b/src/logger/Logger.cpp index 41b6db0f..b99f5664 100644 --- a/src/logger/Logger.cpp +++ b/src/logger/Logger.cpp @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -#include - #include #include #include @@ -37,6 +35,8 @@ #include +#include "config/ConfigReader.hpp" + #include "Logger.hpp" @@ -64,7 +64,7 @@ Logger::Logger() MOD_UI, }; - BOOST_FOREACH(Module module, modules) + for(Module module : modules) _loggers[module].add_attribute("Module", boost::log::attributes::constant< Module >(module)); } @@ -74,19 +74,18 @@ Logger::get(Module module) return _loggers[module]; } - void -Logger::init(const Config& config) +Logger::init() { boost::log::add_common_attributes(); boost::log::register_simple_formatter_factory< Severity, char >("Severity"); - if (config.enableFileLogging) + if (ConfigReader::instance().getBool("main.logger.file.enable")) { boost::log::add_file_log ( - boost::log::keywords::file_name = config.logPath + std::string(".%N"), + boost::log::keywords::file_name = ConfigReader::instance().getString("main.logger.file.path") + std::string(".%N"), boost::log::keywords::rotation_size = 10 * 1024 * 1024, boost::log::keywords::open_mode = std::ios_base::app, boost::log::keywords::auto_flush = true, @@ -100,7 +99,7 @@ Logger::init(const Config& config) ); } - if (config.enableConsoleLogging) + if (ConfigReader::instance().getBool("main.logger.console.enable")) { boost::log::add_console_log(std::cout, boost::log::keywords::format = ( @@ -115,7 +114,7 @@ Logger::init(const Config& config) boost::log::core::get()->set_filter ( - boost::log::expressions::attr("Severity") <= config.minSeverity + boost::log::expressions::attr("Severity") <= ConfigReader::instance().getULong("main.logger.level") ); } diff --git a/src/logger/Logger.hpp b/src/logger/Logger.hpp index ca540f65..98d5c69f 100644 --- a/src/logger/Logger.hpp +++ b/src/logger/Logger.hpp @@ -65,15 +65,8 @@ class Logger static Logger& instance(); - struct Config { - bool enableFileLogging; - bool enableConsoleLogging; - std::string logPath; - Severity minSeverity; - }; - //[ example_tutorial_file_advanced - void init(const Config& config); + void init(); boost::log::sources::severity_logger< Severity >& get(Module module); diff --git a/src/main/main.cpp b/src/main/main.cpp index c63f7247..54af36fb 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -20,11 +20,11 @@ #include #include "config/config.h" -#include "logger/Logger.hpp" - #include "config/ConfigReader.hpp" #include "transcode/AvConvTranscoder.hpp" #include "av/Common.hpp" +#include "logger/Logger.hpp" +#include "cover/CoverArtGrabber.hpp" #include "service/ServiceManager.hpp" #include "service/DatabaseUpdateService.hpp" @@ -61,28 +61,10 @@ int main(int argc, char* argv[]) return EXIT_FAILURE; } - ConfigReader configReader(configFile); + ConfigReader::instance().setFile(configFile); - // Initializa logging facility - { - Logger::Config loggerConfig; - configReader.getLoggerConfig(loggerConfig); - Logger::instance().init(loggerConfig); - } - - { - CoverArt::Grabber::Config config; - configReader.getCoverGrabberConfig(config); - CoverArt::Grabber::instance().init(config); - } - - LMS_LOG(MOD_MAIN, SEV_INFO) << "Reading service configurations..."; - - Service::DatabaseUpdateService::Config dbUpdateConfig; - configReader.getDatabaseUpdateConfig(dbUpdateConfig); - - Service::UserInterfaceService::Config uiConfig; - configReader.getUserInterfaceConfig(uiConfig); + Logger::instance().init(); + CoverArt::Grabber::instance().init(); Service::ServiceManager& serviceManager = Service::ServiceManager::instance(); @@ -93,20 +75,12 @@ int main(int argc, char* argv[]) LMS_LOG(MOD_MAIN, SEV_INFO) << "Starting services..."; - if (dbUpdateConfig.enable) - serviceManager.startService( std::make_shared( dbUpdateConfig ) ); - + serviceManager.startService( std::make_shared() ); + serviceManager.startService( std::make_shared(boost::filesystem::path(argv[0]))); #if defined HAVE_LMSAPI - Service::LmsAPIService::Config lmsAPIConfig; - configReader.getLmsAPIConfig(lmsAPIConfig); - - if (lmsAPIConfig.enable) - serviceManager.startService( std::make_shared( lmsAPIConfig )); + serviceManager.startService( std::make_shared( )); #endif - if (uiConfig.enable) - serviceManager.startService( std::make_shared(boost::filesystem::path(argv[0]), uiConfig)); - LMS_LOG(MOD_MAIN, SEV_NOTICE) << "Now running..."; serviceManager.run(); diff --git a/src/service/DatabaseUpdateService.cpp b/src/service/DatabaseUpdateService.cpp index fb639bf1..73ae911a 100644 --- a/src/service/DatabaseUpdateService.cpp +++ b/src/service/DatabaseUpdateService.cpp @@ -19,18 +19,32 @@ #include +#include "config/ConfigReader.hpp" #include "logger/Logger.hpp" #include "DatabaseUpdateService.hpp" +static std::vector splitStrings(const std::string& source) +{ + std::vector res; + std::istringstream oss(source); + + std::string str; + while(oss >> str) + res.push_back(str); + + return res; +} + namespace Service { -DatabaseUpdateService::DatabaseUpdateService(const Config& config) +DatabaseUpdateService::DatabaseUpdateService() : _metadataParser(), - _databaseUpdater( config.dbPath, _metadataParser) + _databaseUpdater( ConfigReader::instance().getString("main.database.path"), + _metadataParser) { - _databaseUpdater.setAudioExtensions(config.audioExtensions); - _databaseUpdater.setVideoExtensions(config.videoExtensions); + _databaseUpdater.setAudioExtensions(splitStrings(ConfigReader::instance().getString("main.database.audio_extensions"))); + _databaseUpdater.setVideoExtensions(splitStrings(ConfigReader::instance().getString("main.database.video_extensions"))); } void diff --git a/src/service/DatabaseUpdateService.hpp b/src/service/DatabaseUpdateService.hpp index becc74d7..55bfadaa 100644 --- a/src/service/DatabaseUpdateService.hpp +++ b/src/service/DatabaseUpdateService.hpp @@ -36,14 +36,7 @@ class DatabaseUpdateService : public Service typedef std::shared_ptr pointer; - struct Config { - bool enable; - boost::filesystem::path dbPath; - std::vector audioExtensions; - std::vector videoExtensions; - }; - - DatabaseUpdateService(const Config& config); + DatabaseUpdateService(); // Service interface void start(void); diff --git a/src/service/LmsAPIServerService.cpp b/src/service/LmsAPIServerService.cpp index c141c4e0..94c0cbb1 100644 --- a/src/service/LmsAPIServerService.cpp +++ b/src/service/LmsAPIServerService.cpp @@ -17,18 +17,24 @@ * along with LMS. If not, see . */ +#include + +#include "config/ConfigReader.hpp" #include "logger/Logger.hpp" #include "LmsAPIServerService.hpp" namespace Service { -LmsAPIService::LmsAPIService(const Config& config) -: _server(boost::asio::ip::tcp::endpoint(config.address, config.port), - config.sslCertificatePath, - config.sslPrivateKeyPath, - config.sslTempDhPath, - config.dbPath) +LmsAPIService::LmsAPIService() +: _server( + boost::asio::ip::tcp::endpoint( + boost::asio::ip::address::from_string(ConfigReader::instance().getString("remote.listen-endpoint.addr")), + ConfigReader::instance().getULong("remote.listen-endpoint.port")), + ConfigReader::instance().getString("remote.ssl-crypto.cert"), + ConfigReader::instance().getString("remote.ssl-crypto.key"), + ConfigReader::instance().getString("remote.ssl-crypto.dh"), + ConfigReader::instance().getString("main.database.path")) { } diff --git a/src/service/LmsAPIServerService.hpp b/src/service/LmsAPIServerService.hpp index 8fd17f1f..06aaa556 100644 --- a/src/service/LmsAPIServerService.hpp +++ b/src/service/LmsAPIServerService.hpp @@ -21,7 +21,6 @@ #define REMOTE_SERVER_SERVICE_HPP #include -#include #include "config/config.h" @@ -35,17 +34,7 @@ class LmsAPIService : public Service { public: - struct Config { - bool enable; - boost::asio::ip::address address; - unsigned short port; - boost::filesystem::path sslCertificatePath; - boost::filesystem::path sslPrivateKeyPath; - boost::filesystem::path sslTempDhPath; - boost::filesystem::path dbPath; - }; - - LmsAPIService(const Config& config); + LmsAPIService(); void start(void); void stop(void); diff --git a/src/service/UserInterfaceService.cpp b/src/service/UserInterfaceService.cpp index 06353841..cf937897 100644 --- a/src/service/UserInterfaceService.cpp +++ b/src/service/UserInterfaceService.cpp @@ -22,24 +22,23 @@ #include "UserInterfaceService.hpp" #include "ui/LmsApplication.hpp" +#include "config/ConfigReader.hpp" + namespace Service { -UserInterfaceService::UserInterfaceService( boost::filesystem::path runAppPath, const Config& config) +UserInterfaceService::UserInterfaceService( boost::filesystem::path runAppPath) : _server(runAppPath.string()) { std::vector args; args.push_back(runAppPath.string()); - args.push_back("--docroot=" + config.docRootPath.string()); - args.push_back("--approot=" + config.appRootPath.string()); - { - std::ostringstream oss; oss << config.httpsPort; - args.push_back("--https-port=" + oss.str()); - } - args.push_back("--https-address=" + config.httpsAddress.to_string()); - args.push_back("--ssl-certificate=" + config.sslCertificatePath.string()); - args.push_back("--ssl-private-key=" + config.sslPrivateKeyPath.string()); - args.push_back("--ssl-tmp-dh=" + config.sslTempDhPath.string()); + args.push_back("--docroot=" + ConfigReader::instance().getString("ui.resources.docroot")); + args.push_back("--approot=" + ConfigReader::instance().getString("ui.resources.approot")); + args.push_back("--https-port=" + std::to_string( ConfigReader::instance().getULong("ui.listen-endpoint.port"))); + args.push_back("--https-address=" + ConfigReader::instance().getString("ui.listen-endpoint.addr")); + args.push_back("--ssl-certificate=" + ConfigReader::instance().getString("ui.ssl-crypto.cert")); + args.push_back("--ssl-private-key=" + ConfigReader::instance().getString("ui.ssl-crypto.key")); + args.push_back("--ssl-tmp-dh=" + ConfigReader::instance().getString("ui.ssl-crypto.dh")); // Construct argc/argv int argc = args.size(); @@ -55,7 +54,7 @@ UserInterfaceService::UserInterfaceService( boost::filesystem::path runAppPath, _server.setServerConfiguration (argc, const_cast(argv)); // bind entry point - _server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create, _1, config.dbPath)); + _server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create, _1, ConfigReader::instance().getString("main.database.path"))); } diff --git a/src/service/UserInterfaceService.hpp b/src/service/UserInterfaceService.hpp index 2ae09b51..c023a3f6 100644 --- a/src/service/UserInterfaceService.hpp +++ b/src/service/UserInterfaceService.hpp @@ -21,7 +21,6 @@ #define WEB_SERVER_SERVICE_HPP #include -#include #include @@ -33,20 +32,7 @@ class UserInterfaceService : public Service { public: - struct Config { - bool enable; - boost::filesystem::path docRootPath; - boost::filesystem::path appRootPath; - unsigned short httpsPort; - boost::asio::ip::address httpsAddress; - boost::filesystem::path sslCertificatePath; - boost::filesystem::path sslPrivateKeyPath; - boost::filesystem::path sslTempDhPath; - boost::filesystem::path dbPath; - }; - - UserInterfaceService(boost::filesystem::path runAppPath, - const Config& config); + UserInterfaceService(boost::filesystem::path runAppPath); void start(void); void stop(void); diff --git a/src/transcode/AvConvTranscoder.cpp b/src/transcode/AvConvTranscoder.cpp index 655bffc6..70b056fa 100644 --- a/src/transcode/AvConvTranscoder.cpp +++ b/src/transcode/AvConvTranscoder.cpp @@ -157,14 +157,12 @@ AvConvTranscoder::AvConvTranscoder(const Parameters& parameters) // See boost process FAQ { boost::lock_guard lock(_mutex); - std::vector ranges = { 3, 1024 }; // fd range to be closed _child = std::make_shared( boost::process::execute( boost::process::initializers::run_exe(_avConvPath), boost::process::initializers::set_cmd_line(oss.str()), boost::process::initializers::bind_stdout(sink), - boost::process::initializers::close_fd(STDIN_FILENO), - boost::process::initializers::close_fds(ranges) + boost::process::initializers::close_fds_if([](int fd) { return fd != STDOUT_FILENO;}) ) ); } diff --git a/src/ui/audio/desktop/DesktopAudio.cpp b/src/ui/audio/desktop/DesktopAudio.cpp index 7f5bc9a9..3aa25975 100644 --- a/src/ui/audio/desktop/DesktopAudio.cpp +++ b/src/ui/audio/desktop/DesktopAudio.cpp @@ -102,8 +102,13 @@ _playQueue(nullptr) Wt::WPushButton* addBtn = new Wt::WPushButton("Add"); addBtn->setStyleClass("btn-sm"); trackControls->addWidget(addBtn); - trackControls->addWidget(new Wt::WText("Total duration: "), 1); + Wt::WText *statsText = new Wt::WText(); + statsText->setStyleClass("vertical-align"); + trackControls->addWidget(statsText, 1); + _trackView->statsUpdated().connect(std::bind([=] (Wt::WString stats) { + statsText->setText(stats); + }, std::placeholders::_1)); trackLayout->addLayout(trackControls); @@ -111,6 +116,7 @@ _playQueue(nullptr) _filterChain.addFilter(_trackView); + _playQueue = new PlayQueue(); // Playlist/PlayQueue @@ -216,6 +222,9 @@ _playQueue(nullptr) playlistRefreshMenus(); + + // Initially, search for everything + _filterChain.searchKeyword(""); } void diff --git a/src/ui/audio/desktop/Filter.hpp b/src/ui/audio/desktop/Filter.hpp index 194f2867..5feb120a 100644 --- a/src/ui/audio/desktop/Filter.hpp +++ b/src/ui/audio/desktop/Filter.hpp @@ -30,14 +30,6 @@ namespace Desktop { class Filter { public: - - struct Constraint { - std::vector search; - - typedef std::map > ColumnValues; - ColumnValues columnValues; - }; - Filter() {} virtual ~Filter() {} diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index 3f1e6fc8..dc38bd31 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -291,6 +291,8 @@ _trackSelector(new TrackSelector()) this->setColumnWidth(COLUMN_ID_NAME, 240); this->setLayoutSizeAware(true); + this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal); + this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); this->setColumnHidden(COLUMN_ID_TRACK_ID, true); @@ -501,12 +503,17 @@ PlayQueue::delSelected(void) int minId = _model->rowCount(); Wt::WModelIndexSet indexSet = this->selectedIndexes(); - BOOST_REVERSE_FOREACH(Wt::WModelIndex index, indexSet) - { - _model->removeRow(index.row()); - if (index.row() < minId) - minId = index.row(); - } + // Make sure to delete entries in the reverse order + std::vector rowIds; + for (Wt::WModelIndex index : indexSet) + rowIds.push_back(index.row()); + + std::sort(rowIds.begin(), rowIds.end(), std::greater()); + if (!rowIds.empty()) + minId = rowIds.back(); + + for (int rowId : rowIds) + _model->removeRow(rowId); // If the current played track is removed, make sure to unselect it _trackSelector->setSize(_model->rowCount()); diff --git a/src/ui/audio/desktop/TableFilter.cpp b/src/ui/audio/desktop/TableFilter.cpp index 2e410870..7e11ae93 100644 --- a/src/ui/audio/desktop/TableFilter.cpp +++ b/src/ui/audio/desktop/TableFilter.cpp @@ -17,8 +17,6 @@ * along with LMS. If not, see . */ -#include - #include #include "database/Types.hpp" @@ -50,7 +48,10 @@ TableFilterGenre::TableFilterGenre(Wt::WContainerWidget* parent) this->selectionChanged().connect(this, &TableFilterGenre::emitUpdate); - setLayoutSizeAware(true); + this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal); + this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); + + this->setLayoutSizeAware(true); _queryModel.setBatchSize(100); @@ -122,7 +123,10 @@ TableFilterArtist::TableFilterArtist(Wt::WContainerWidget* parent) this->selectionChanged().connect(this, &TableFilterArtist::emitUpdate); - setLayoutSizeAware(true); + this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal); + this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); + + this->setLayoutSizeAware(true); _queryModel.setBatchSize(100); @@ -163,8 +167,8 @@ TableFilterArtist::getConstraint(SearchFilter& filter) { Wt::WModelIndexSet indexSet = this->selectedIndexes(); - BOOST_FOREACH(Wt::WModelIndex index, indexSet) { - + for (Wt::WModelIndex index : indexSet) + { if (!index.isValid()) continue; @@ -200,7 +204,10 @@ TableFilterRelease::TableFilterRelease(Wt::WContainerWidget* parent) this->selectionChanged().connect(this, &TableFilterRelease::emitUpdate); - setLayoutSizeAware(true); + this->setOverflow(Wt::WContainerWidget::OverflowHidden, Wt::Horizontal); + this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); + + this->setLayoutSizeAware(true); _queryModel.setBatchSize(100); diff --git a/src/ui/audio/desktop/TrackView.cpp b/src/ui/audio/desktop/TrackView.cpp index b98a0198..26f89f04 100644 --- a/src/ui/audio/desktop/TrackView.cpp +++ b/src/ui/audio/desktop/TrackView.cpp @@ -17,9 +17,6 @@ * along with LMS. If not, see . */ - -#include - #include #include @@ -31,6 +28,8 @@ namespace UserInterface { namespace Desktop { +using namespace Database; + TrackView::TrackView(Wt::WContainerWidget* parent) : Wt::WTableView( parent ) { @@ -48,9 +47,9 @@ TrackView::TrackView(Wt::WContainerWidget* parent) "Genres", }; - Database::SearchFilter filter; + SearchFilter filter; - Database::Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames); + Track::updateUIQueryModel(DboSession(), _queryModel, filter, columnNames); _queryModel.setBatchSize(300); @@ -68,7 +67,7 @@ TrackView::TrackView(Wt::WContainerWidget* parent) this->setColumnWidth(6, 70); // Date this->setColumnWidth(7, 70); // Original Date this->setColumnWidth(8, 180); // Genres -// this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); + this->setOverflow(Wt::WContainerWidget::OverflowScroll, Wt::Vertical); // Duration display { @@ -107,15 +106,47 @@ TrackView::TrackView(Wt::WContainerWidget* parent) } +void +TrackView::emitStats(const SearchFilter& filter) +{ + Wt::Dbo::Transaction transaction (DboSession()); + + // Update stats on the view + Track::StatsQueryResult stats = Track::getStats(DboSession(), filter); + + transaction.commit(); + + int nbTracks = stats.get<0>(); + boost::posix_time::time_duration totalDuration = stats.get<1>(); + + std::ostringstream oss; + + oss << nbTracks << " track" << (nbTracks > 1 ? "s" : "") << ", "; + + if (totalDuration.hours() >= 24) + { + auto days = totalDuration.hours() / 24; + oss << days << " day" << (days > 1 ? "s " : " "); + } + + oss << std::setw(2) << std::setfill('0') << totalDuration.hours() % 24 + << ":" << std::setw(2) << std::setfill('0') << totalDuration.minutes() + << ":" << std::setw(2) << std::setfill('0') << totalDuration.seconds(); + + _sigStatsUpdated.emit(Wt::WString(oss.str())); +} + // Set constraints created by parent filters void -TrackView::refresh(Database::SearchFilter& filter) +TrackView::refresh(SearchFilter& filter) { - Database::Track::updateUIQueryModel(DboSession(), _queryModel, filter); + Track::updateUIQueryModel(DboSession(), _queryModel, filter); + + emitStats(filter); } void -TrackView::getSelectedTracks(std::vector& track_ids) +TrackView::getSelectedTracks(std::vector& track_ids) { LMS_LOG(MOD_UI, SEV_DEBUG) << "Getting selected tracks..."; @@ -126,7 +157,7 @@ TrackView::getSelectedTracks(std::vector& track_ids) if (!index.isValid()) continue; - Database::Track::id_type id = _queryModel.resultRow( index.row() ).get<0>(); + Track::id_type id = _queryModel.resultRow( index.row() ).get<0>(); track_ids.push_back(id); } @@ -157,16 +188,16 @@ TrackView::getFirstSelectedTrackPosition(void) } void -TrackView::getTracks(std::vector& trackIds) +TrackView::getTracks(std::vector& trackIds) { LMS_LOG(MOD_UI, SEV_DEBUG) << "Getting all tracks..."; Wt::Dbo::Transaction transaction(DboSession()); - Wt::Dbo::collection results = _queryModel.query(); + Wt::Dbo::collection results = _queryModel.query(); for (auto it = results.begin(); it != results.end(); ++it) { - Database::Track::id_type id = it->get<0>(); + Track::id_type id = it->get<0>(); trackIds.push_back(id); } diff --git a/src/ui/audio/desktop/TrackView.hpp b/src/ui/audio/desktop/TrackView.hpp index f922589e..2fef0556 100644 --- a/src/ui/audio/desktop/TrackView.hpp +++ b/src/ui/audio/desktop/TrackView.hpp @@ -55,12 +55,17 @@ class TrackView : public Wt::WTableView, public Filter void getTracks(std::vector& track_ids); typedef Wt::Signal SigTrackDoubleClicked; + typedef Wt::Signal SigStatsUpdated; SigTrackDoubleClicked& trackDoubleClicked() { return _sigTrackDoubleClicked; } + SigStatsUpdated& statsUpdated() { return _sigStatsUpdated; } private: SigTrackDoubleClicked _sigTrackDoubleClicked; + SigStatsUpdated _sigStatsUpdated; + + void emitStats(const Database::SearchFilter& filter); typedef Database::Track::UIQueryResult ResultType; Wt::Dbo::QueryModel< ResultType > _queryModel; diff --git a/src/ui/resource/AvConvTranscodeStreamResource.cpp b/src/ui/resource/AvConvTranscodeStreamResource.cpp index 025455a2..7eb81ad8 100644 --- a/src/ui/resource/AvConvTranscodeStreamResource.cpp +++ b/src/ui/resource/AvConvTranscodeStreamResource.cpp @@ -47,7 +47,7 @@ AvConvTranscodeStreamResource::handleRequest(const Wt::Http::Request& request, // see if this request is for a continuation: Wt::Http::ResponseContinuation *continuation = request.continuation(); - LMS_LOG(MOD_UI, SEV_DEBUG) << "Handling new request. Continuation = " << std::boolalpha << continuation; + LMS_LOG(MOD_UI, SEV_DEBUG) << "Handling new request. Continuation = " << continuation; std::shared_ptr transcoder; if (continuation) diff --git a/test/CheckDbBasics.cpp b/test/CheckDbBasics.cpp index e1fbfaa9..b67101b7 100644 --- a/test/CheckDbBasics.cpp +++ b/test/CheckDbBasics.cpp @@ -157,6 +157,9 @@ int main(void) std::vector res = Artist::getByFilter(db.getSession(), filter, -1, -1); assert(res.size() == 1); assert(res.front().id() == 1); + + // Make sure artist has a release + assert(res.front()->getReleases().size() == 1); } // Select Release by name @@ -179,6 +182,9 @@ int main(void) assert(res.size() == 1); assert(res.front().id() == 1); assert(res.front()->getName() == "release01"); + + // Make sure release has an artist + assert(res.front()->getArtists().size() == 1); } // Select genre by name diff --git a/test/Makefile.am b/test/Makefile.am index d551d4f7..6b3dddd6 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -13,21 +13,19 @@ remote_client_SOURCES = \ nodist_remote_client_SOURCES = \ $(builddir)/auth.pb.cc \ $(builddir)/collection.pb.cc \ - $(builddir)/common.pb.cc \ $(builddir)/media.pb.cc \ $(builddir)/messages.pb.cc BUILT_SOURCES = \ $(builddir)/auth.pb.h \ $(builddir)/collection.pb.h \ - $(builddir)/common.pb.h \ $(builddir)/media.pb.h \ $(builddir)/messages.pb.h remote_client_CXXFLAGS=-std=c++11 -Wall -Wextra -DBOOST_LOG_DYN_LINK -I$(top_srcdir)/src -%.pb.cc %.pb.h: $(top_srcdir)/src/lms-api/proto/%.proto - $(PROTOC) --proto_path=$(top_srcdir)/src/lms-api/proto/ --cpp_out=$(builddir)/ $^ +%.pb.cc %.pb.h: $(top_srcdir)/lms-api/proto/%.proto + $(PROTOC) --proto_path=$(top_srcdir)/lms-api/proto/ --cpp_out=$(builddir)/ $^ endif diff --git a/test/RemoteClient.cpp b/test/RemoteClient.cpp index dce3e641..062547fb 100644 --- a/test/RemoteClient.cpp +++ b/test/RemoteClient.cpp @@ -27,7 +27,7 @@ #include #include -#include "remote/messages/Header.hpp" +#include "lms-api/messages/Header.hpp" #include "messages.pb.h" #include "TestDatabase.hpp" @@ -123,7 +123,7 @@ struct SearchFilter std::vector trackIds; }; -void SearchFilterToRequest(const SearchFilter& filter, Remote::AudioCollectionRequest_SearchFilter& request) +void SearchFilterToRequest(const SearchFilter& filter, LmsAPI::AudioCollectionRequest_SearchFilter& request) { for (uint64_t id : filter.artistIds) request.add_artist_id(id); @@ -174,11 +174,11 @@ class TestClient std::size_t nbArtists = 0; // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage_Type_AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage_Type_AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest_Type_TypeGetArtistList); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest_Type_TypeGetArtistList); request.mutable_audio_collection_request()->mutable_get_artists()->mutable_batch_parameter()->set_size(size); request.mutable_audio_collection_request()->mutable_get_artists()->mutable_batch_parameter()->set_offset(offset); SearchFilterToRequest(filter, *request.mutable_audio_collection_request()->mutable_get_artists()->mutable_search_filter()); @@ -186,7 +186,7 @@ class TestClient sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -198,7 +198,7 @@ class TestClient for (int i = 0; i < response.audio_collection_response().artist_list().artists_size(); ++i) { - const Remote::AudioCollectionResponse_Artist& respArtist = response.audio_collection_response().artist_list().artists(i); + const LmsAPI::AudioCollectionResponse_Artist& respArtist = response.audio_collection_response().artist_list().artists(i); if (!respArtist.has_id()) throw std::runtime_error("no id!"); @@ -235,11 +235,11 @@ class TestClient std::size_t nbAdded = 0; // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage_Type_AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage_Type_AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest_Type_TypeGetGenreList); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest_Type_TypeGetGenreList); request.mutable_audio_collection_request()->mutable_get_genres()->mutable_batch_parameter()->set_size(size); request.mutable_audio_collection_request()->mutable_get_genres()->mutable_batch_parameter()->set_offset(offset); SearchFilterToRequest(filter, *request.mutable_audio_collection_request()->mutable_get_genres()->mutable_search_filter()); @@ -247,7 +247,7 @@ class TestClient sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -259,7 +259,7 @@ class TestClient for (int i = 0; i < response.audio_collection_response().genre_list().genres_size(); ++i) { - const Remote::AudioCollectionResponse_Genre& respGenre = response.audio_collection_response().genre_list().genres(i); + const LmsAPI::AudioCollectionResponse_Genre& respGenre = response.audio_collection_response().genre_list().genres(i); if (!respGenre.has_id()) throw std::runtime_error("no genre id!"); @@ -293,11 +293,11 @@ class TestClient std::size_t nbAdded = 0; // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage_Type_AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage_Type_AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest_Type_TypeGetReleaseList); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest_Type_TypeGetReleaseList); request.mutable_audio_collection_request()->mutable_get_releases()->mutable_batch_parameter()->set_size(size); request.mutable_audio_collection_request()->mutable_get_releases()->mutable_batch_parameter()->set_offset(offset); SearchFilterToRequest(filter, *request.mutable_audio_collection_request()->mutable_get_releases()->mutable_search_filter()); @@ -305,14 +305,14 @@ class TestClient sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message if (!response.audio_collection_response().has_type()) throw std::runtime_error("Missing type!"); -// if (!response.audio_collection_response().type() != Remote::ServerMessage::AudioCollectionResponse::TypeReleaseList) +// if (!response.audio_collection_response().type() != LmsAPI::ServerMessage::AudioCollectionResponse::TypeReleaseList) // throw std::runtime_error("Bad type!"); if (!response.has_audio_collection_response()) @@ -323,7 +323,7 @@ class TestClient for (int i = 0; i < response.audio_collection_response().release_list().releases_size(); ++i) { - const Remote::AudioCollectionResponse_Release& respRelease = response.audio_collection_response().release_list().releases(i); + const LmsAPI::AudioCollectionResponse_Release& respRelease = response.audio_collection_response().release_list().releases(i); if (!respRelease.has_id()) throw std::runtime_error("no id!"); @@ -355,11 +355,11 @@ class TestClient std::size_t nbAdded = 0; // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage_Type_AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage_Type_AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest_Type_TypeGetTrackList); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest_Type_TypeGetTrackList); request.mutable_audio_collection_request()->mutable_get_tracks()->mutable_batch_parameter()->set_size(size); request.mutable_audio_collection_request()->mutable_get_tracks()->mutable_batch_parameter()->set_offset(offset); SearchFilterToRequest(filter, *request.mutable_audio_collection_request()->mutable_get_tracks()->mutable_search_filter()); @@ -367,7 +367,7 @@ class TestClient sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -379,7 +379,7 @@ class TestClient for (int i = 0; i < response.audio_collection_response().track_list().tracks_size(); ++i) { - const Remote::AudioCollectionResponse_Track& respTrack = response.audio_collection_response().track_list().tracks(i);; + const LmsAPI::AudioCollectionResponse_Track& respTrack = response.audio_collection_response().track_list().tracks(i);; TrackInfo track; track.id = respTrack.id(); @@ -426,18 +426,18 @@ class TestClient void getCoverTrack(std::vector& coverArt, uint64_t trackId) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage::AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest::TypeGetCoverArt); - request.mutable_audio_collection_request()->mutable_get_cover_art()->set_type( Remote::AudioCollectionRequest::GetCoverArt::TypeGetCoverArtTrack); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest::TypeGetCoverArt); + request.mutable_audio_collection_request()->mutable_get_cover_art()->set_type( LmsAPI::AudioCollectionRequest::GetCoverArt::TypeGetCoverArtTrack); request.mutable_audio_collection_request()->mutable_get_cover_art()->set_track_id( trackId ); request.mutable_audio_collection_request()->mutable_get_cover_art()->set_size( 256 ); sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -457,19 +457,19 @@ class TestClient void getCoverRelease(std::vector& coverArt, uint64_t releaseId) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage::AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest::TypeGetCoverArt); - request.mutable_audio_collection_request()->mutable_get_cover_art()->set_type( Remote::AudioCollectionRequest::GetCoverArt::TypeGetCoverArtRelease); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest::TypeGetCoverArt); + request.mutable_audio_collection_request()->mutable_get_cover_art()->set_type( LmsAPI::AudioCollectionRequest::GetCoverArt::TypeGetCoverArtRelease); request.mutable_audio_collection_request()->mutable_get_cover_art()->set_release_id( releaseId ); request.mutable_audio_collection_request()->mutable_get_cover_art()->set_size( 256 ); sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -490,16 +490,16 @@ class TestClient std::string getRevision(void) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::AudioCollectionRequest ); + request.set_type( LmsAPI::ClientMessage::AudioCollectionRequest ); - request.mutable_audio_collection_request()->set_type( Remote::AudioCollectionRequest::TypeGetRevision); + request.mutable_audio_collection_request()->set_type( LmsAPI::AudioCollectionRequest::TypeGetRevision); sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -515,18 +515,18 @@ class TestClient bool login(const std::string& username, const std::string& password) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::AuthRequest ); + request.set_type( LmsAPI::ClientMessage::AuthRequest ); - request.mutable_auth_request()->set_type( Remote::AuthRequest::TypePassword); + request.mutable_auth_request()->set_type( LmsAPI::AuthRequest::TypePassword); request.mutable_auth_request()->mutable_password()->set_user_login( username ); request.mutable_auth_request()->mutable_password()->set_user_password( password ); sendMsg(request); // Receive responses - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); // Process message @@ -538,11 +538,11 @@ class TestClient switch( response.auth_response().password_result().type()) { - case Remote::AuthResponse::PasswordResult::TypePasswordValid: + case LmsAPI::AuthResponse::PasswordResult::TypePasswordValid: return true; - case Remote::AuthResponse::PasswordResult::TypePasswordInvalid: + case LmsAPI::AuthResponse::PasswordResult::TypePasswordInvalid: return false; - case Remote::AuthResponse::PasswordResult::TypeLoginThrottling: + case LmsAPI::AuthResponse::PasswordResult::TypeLoginThrottling: if (response.auth_response().password_result().has_delay()) std::cerr << "Has to wait for " << response.auth_response().password_result().delay() << " seconds" << std::endl; @@ -571,24 +571,24 @@ class TestClient { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage_Type_MediaRequest); + request.set_type( LmsAPI::ClientMessage_Type_MediaRequest); - request.mutable_media_request()->set_type( Remote::MediaRequest_Type_TypeMediaPrepare); - request.mutable_media_request()->mutable_prepare()->set_type(Remote::MediaRequest_Prepare_Type_AudioRequest); + request.mutable_media_request()->set_type( LmsAPI::MediaRequest_Type_TypeMediaPrepare); + request.mutable_media_request()->mutable_prepare()->set_type(LmsAPI::MediaRequest_Prepare_Type_AudioRequest); // Set fields request.mutable_media_request()->mutable_prepare()->mutable_audio()->set_track_id( audioId ); - request.mutable_media_request()->mutable_prepare()->mutable_audio()->set_codec_type( Remote::MediaRequest::Prepare::AudioCodecTypeOGA ); - request.mutable_media_request()->mutable_prepare()->mutable_audio()->set_bitrate( Remote::MediaRequest::Prepare::AudioBitrate_64_kbps ); + request.mutable_media_request()->mutable_prepare()->mutable_audio()->set_codec_type( LmsAPI::MediaRequest::Prepare::AudioCodecTypeOGA ); + request.mutable_media_request()->mutable_prepare()->mutable_audio()->set_bitrate( LmsAPI::MediaRequest::Prepare::AudioBitrate_64_kbps ); std::cout << "Sending prepare request" << std::endl; sendMsg(request); std::cout << "Waiting for response" << std::endl; // Receive response - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); std::cout << "Got a response" << std::endl; @@ -616,11 +616,11 @@ class TestClient std::size_t mediaGetPart(uint32_t handle, std::vector& data) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::MediaRequest); + request.set_type( LmsAPI::ClientMessage::MediaRequest); - request.mutable_media_request()->set_type( Remote::MediaRequest::TypeMediaGetPart); + request.mutable_media_request()->set_type( LmsAPI::MediaRequest::TypeMediaGetPart); request.mutable_media_request()->mutable_get_part()->set_handle(handle); request.mutable_media_request()->mutable_get_part()->set_requested_data_size(65536); @@ -629,7 +629,7 @@ class TestClient std::cout << "Waiting for response" << std::endl; // Receive response - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); std::cout << "Got a response" << std::endl; @@ -638,7 +638,7 @@ class TestClient throw std::runtime_error("not an media response"); // Process message - if (response.media_response().type() != Remote::MediaResponse::TypePartResult) + if (response.media_response().type() != LmsAPI::MediaResponse::TypePartResult) throw std::runtime_error("GetPart: not a Part response!"); if (!response.media_response().has_part_result()) @@ -652,17 +652,17 @@ class TestClient void mediaTerminate( uint32_t handle ) { // Send request - Remote::ClientMessage request; + LmsAPI::ClientMessage request; - request.set_type( Remote::ClientMessage::MediaRequest); + request.set_type( LmsAPI::ClientMessage::MediaRequest); - request.mutable_media_request()->set_type( Remote::MediaRequest::TypeMediaTerminate); + request.mutable_media_request()->set_type( LmsAPI::MediaRequest::TypeMediaTerminate); request.mutable_media_request()->mutable_terminate()->set_handle(handle); sendMsg(request); // Receive response - Remote::ServerMessage response; + LmsAPI::ServerMessage response; recvMsg(response); @@ -684,18 +684,18 @@ class TestClient if (message.SerializeToOstream(&os)) { - if (_outputStreamBuf.size() > Remote::Header::max_data_size) + if (_outputStreamBuf.size() > LmsAPI::Header::max_data_size) { - std::ostringstream oss; oss << "Message too big = " << _outputStreamBuf.size() << " bytes! (max is " << Remote::Header::max_data_size << ")" << std::endl; + std::ostringstream oss; oss << "Message too big = " << _outputStreamBuf.size() << " bytes! (max is " << LmsAPI::Header::max_data_size << ")" << std::endl; throw std::runtime_error("Message to big!"); } // Send message header - std::array headerBuffer; + std::array headerBuffer; // Generate header content { - Remote::Header header; + LmsAPI::Header header; header.setDataSize(_outputStreamBuf.size()); header.to_buffer(headerBuffer); } @@ -725,18 +725,18 @@ class TestClient { // reserve bytes in output sequence - boost::asio::streambuf::mutable_buffers_type bufs = _inputStreamBuf.prepare(Remote::Header::size); + boost::asio::streambuf::mutable_buffers_type bufs = _inputStreamBuf.prepare(LmsAPI::Header::size); std::size_t n = boost::asio::read(_socket, bufs, - boost::asio::transfer_exactly(Remote::Header::size)); + boost::asio::transfer_exactly(LmsAPI::Header::size)); - assert(n == Remote::Header::size); + assert(n == LmsAPI::Header::size); _inputStreamBuf.commit(n); } - Remote::Header header; + LmsAPI::Header header; if (!header.from_istream(is)) throw std::runtime_error("Cannot read header from buffer!");