Prepare multi cluster tag parsing

This commit is contained in:
emeric
2018-03-01 22:20:53 +01:00
parent d6f0e34b62
commit 9cf89893e0
20 changed files with 504 additions and 522 deletions
+4
View File
@@ -12,6 +12,8 @@
- handle access rights problems (instead of aborting) - handle access rights problems (instead of aborting)
- add a global play counter for tracks. This will help people to spot most popular files - add a global play counter for tracks. This will help people to spot most popular files
- Use the WServer::post method to notify the end of the database scan? (with results?) - Use the WServer::post method to notify the end of the database scan? (with results?)
- Make clusters based on different metadata (albumgrouping, genre, mood, etc.). Make it configurable somewhere as it is very difficult to handle them all?
- Use albumartist when available (useful for compilations)
[Metadata] [Metadata]
- WMA covers: add support - WMA covers: add support
@@ -31,6 +33,8 @@
- Add a hint for the user to get the nature of tag (created from genre ? created from audio features?) - Add a hint for the user to get the nature of tag (created from genre ? created from audio features?)
- Implement a play queue - Implement a play queue
- Implement a decent player - Implement a decent player
- Tags cloud for artist/release
- Dedicate a color for each tag type
[REST API] [REST API]
- Make a dedicated REST API. Maybe use the SubSonic API or Ampache API? - Make a dedicated REST API. Maybe use the SubSonic API or Ampache API?
+1 -3
View File
@@ -15,14 +15,12 @@ lms_SOURCES = \
$(srcdir)/database/SqlQuery.cpp \ $(srcdir)/database/SqlQuery.cpp \
$(srcdir)/database/Track.cpp \ $(srcdir)/database/Track.cpp \
$(srcdir)/database/User.cpp \ $(srcdir)/database/User.cpp \
$(srcdir)/database/updater/DatabaseUpdater.cpp \
$(srcdir)/database/updater/DatabaseFeatureExtractor.cpp \
$(srcdir)/database/updater/DatabaseHighLevelCluster.cpp \
$(srcdir)/feature/FeatureExtractor.cpp \ $(srcdir)/feature/FeatureExtractor.cpp \
$(srcdir)/feature/FeatureStore.cpp \ $(srcdir)/feature/FeatureStore.cpp \
$(srcdir)/image/Image.cpp \ $(srcdir)/image/Image.cpp \
$(srcdir)/metadata/AvFormat.cpp \ $(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/metadata/TagLibParser.cpp \ $(srcdir)/metadata/TagLibParser.cpp \
$(srcdir)/scanner/MediaScanner.cpp \
$(srcdir)/ui/ArtistView.cpp \ $(srcdir)/ui/ArtistView.cpp \
$(srcdir)/ui/ArtistsView.cpp \ $(srcdir)/ui/ArtistsView.cpp \
$(srcdir)/ui/Explore.cpp \ $(srcdir)/ui/Explore.cpp \
+13 -12
View File
@@ -17,6 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <boost/make_unique.hpp>
#include <Wt/Dbo/FixedSqlConnectionPool> #include <Wt/Dbo/FixedSqlConnectionPool>
#include <Wt/Dbo/backend/Sqlite3> #include <Wt/Dbo/backend/Sqlite3>
@@ -83,22 +85,25 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
_session.mapClass<Database::Artist>("artist"); _session.mapClass<Database::Artist>("artist");
_session.mapClass<Database::Cluster>("cluster"); _session.mapClass<Database::Cluster>("cluster");
_session.mapClass<Database::Track>("track"); _session.mapClass<Database::ClusterType>("cluster_type");
_session.mapClass<Database::MediaDirectory>("media_directory");
_session.mapClass<Database::Playlist>("playlist"); _session.mapClass<Database::Playlist>("playlist");
_session.mapClass<Database::PlaylistEntry>("playlist_entry"); _session.mapClass<Database::PlaylistEntry>("playlist_entry");
_session.mapClass<Database::Release>("release"); _session.mapClass<Database::Release>("release");
_session.mapClass<Database::MediaDirectory>("media_directory");
_session.mapClass<Database::Setting>("setting"); _session.mapClass<Database::Setting>("setting");
_session.mapClass<Database::Track>("track");
_session.mapClass<Database::User>("user");
_session.mapClass<Database::AuthInfo>("auth_info"); _session.mapClass<Database::AuthInfo>("auth_info");
_session.mapClass<Database::AuthInfo::AuthIdentityType>("auth_identity"); _session.mapClass<Database::AuthInfo::AuthIdentityType>("auth_identity");
_session.mapClass<Database::AuthInfo::AuthTokenType>("auth_token"); _session.mapClass<Database::AuthInfo::AuthTokenType>("auth_token");
_session.mapClass<Database::User>("user");
try { try {
Wt::Dbo::Transaction transaction(_session); Wt::Dbo::Transaction transaction(_session);
_session.createTables(); _session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
} }
catch(std::exception& e) { catch(std::exception& e) {
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what(); LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
@@ -108,21 +113,17 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
Wt::Dbo::Transaction transaction(_session); Wt::Dbo::Transaction transaction(_session);
// Indexes // Indexes
_session.execute("PRAGMA journal_mode=WAL"); // _session.execute("PRAGMA journal_mode=WAL");
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)"); _session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(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 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_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 track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_idx ON cluster(type)"); _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)");
// TODO move this
// Default values // Default values
if (!Setting::exists(_session, "audio_file_extensions"))
Setting::setString(_session, "audio_file_extensions", ".mp3 .ogg .oga .aac .m4a .flac .wav .wma .aif .aiff .ape .mpc .shn" );
if (!Setting::exists(_session, "video_file_extensions"))
Setting::setString(_session, "video_file_extensions", ".flv .avi .mpg .mpeg .mp4 .m4v .mkv .mov .wmv .ogv .divx .m2ts");
if (!Setting::exists(_session, "tags_highlevel_acousticbrainz")) if (!Setting::exists(_session, "tags_highlevel_acousticbrainz"))
Setting::setBool(_session, "tags_highlevel_acousticbrainz", true); Setting::setBool(_session, "tags_highlevel_acousticbrainz", true);
@@ -184,7 +185,7 @@ Handler::createConnectionPool(boost::filesystem::path p)
Wt::Dbo::backend::Sqlite3 *connection = new Wt::Dbo::backend::Sqlite3(p.string()); Wt::Dbo::backend::Sqlite3 *connection = new Wt::Dbo::backend::Sqlite3(p.string());
connection->executeSql("pragma journal_mode=WAL"); // connection->executeSql("pragma journal_mode=WAL");
connection->setProperty("show-queries", "true"); connection->setProperty("show-queries", "true");
return new Wt::Dbo::FixedSqlConnectionPool(connection, 1); return new Wt::Dbo::FixedSqlConnectionPool(connection, 1);
+1
View File
@@ -21,6 +21,7 @@
#define DATABASE_HANDLER_HPP #define DATABASE_HANDLER_HPP
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <memory>
#include <Wt/Dbo/Dbo> #include <Wt/Dbo/Dbo>
#include <Wt/Dbo/SqlConnectionPool> #include <Wt/Dbo/SqlConnectionPool>
+4 -19
View File
@@ -23,16 +23,15 @@
namespace Database { namespace Database {
MediaDirectory::MediaDirectory(boost::filesystem::path p, Type type) MediaDirectory::MediaDirectory(boost::filesystem::path p)
: _type(type), : _path(stringTrimEnd(p.string(), "/\\"))
_path(stringTrimEnd(p.string(), "/\\"))
{ {
} }
MediaDirectory::pointer MediaDirectory::pointer
MediaDirectory::create(Wt::Dbo::Session& session, boost::filesystem::path p, Type type) MediaDirectory::create(Wt::Dbo::Session& session, boost::filesystem::path p)
{ {
return session.add( new MediaDirectory( p, type ) ); return session.add( new MediaDirectory(p) );
} }
void void
@@ -50,20 +49,6 @@ MediaDirectory::getAll(Wt::Dbo::Session& session)
return std::vector<MediaDirectory::pointer>(res.begin(), res.end()); return std::vector<MediaDirectory::pointer>(res.begin(), res.end());
} }
std::vector<MediaDirectory::pointer>
MediaDirectory::getByType(Wt::Dbo::Session& session, Type type)
{
Wt::Dbo::collection< MediaDirectory::pointer > res = session.find<MediaDirectory>().where("type = ?").bind (type);
return std::vector<MediaDirectory::pointer>(res.begin(), res.end());
}
MediaDirectory::pointer
MediaDirectory::get(Wt::Dbo::Session& session, boost::filesystem::path p, Type type)
{
return session.find<MediaDirectory>().where("path = ?").where("type = ?").bind( p.string()).bind(type);
}
boost::filesystem::path boost::filesystem::path
MediaDirectory::getPath(void) const MediaDirectory::getPath(void) const
{ {
+3 -13
View File
@@ -31,39 +31,29 @@ namespace Database {
class MediaDirectory class MediaDirectory
{ {
public: public:
typedef Wt::Dbo::ptr<MediaDirectory> pointer; typedef Wt::Dbo::ptr<MediaDirectory> pointer;
enum Type {
Audio = 1,
};
MediaDirectory() {} MediaDirectory() {}
MediaDirectory(boost::filesystem::path p, Type type); MediaDirectory(boost::filesystem::path p);
// Accessors // Accessors
static pointer create(Wt::Dbo::Session& session, boost::filesystem::path p, Type type); static pointer create(Wt::Dbo::Session& session, boost::filesystem::path p);
static std::vector<MediaDirectory::pointer> getAll(Wt::Dbo::Session& session); static std::vector<MediaDirectory::pointer> getAll(Wt::Dbo::Session& session);
static std::vector<MediaDirectory::pointer> getByType(Wt::Dbo::Session& session, Type type);
static pointer get(Wt::Dbo::Session& session, boost::filesystem::path p, Type type);
static void eraseAll(Wt::Dbo::Session& session); static void eraseAll(Wt::Dbo::Session& session);
static void eraseByPath(Wt::Dbo::Session& session, boost::filesystem::path p);
Type getType(void) const { return _type; }
boost::filesystem::path getPath(void) const; boost::filesystem::path getPath(void) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _path, "path"); Wt::Dbo::field(a, _path, "path");
} }
private: private:
Type _type;
std::string _path; std::string _path;
}; };
} // namespace Database } // namespace Database
+59 -38
View File
@@ -224,44 +224,24 @@ Cluster::Cluster()
{ {
} }
Cluster::Cluster(std::string type, std::string name) Cluster::Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name)
: : _name(std::string(name, 0, _maxNameLength)),
_type( std::string(type, 0, _maxTypeLength)), _clusterType(type)
_name( std::string(name, 0, _maxNameLength))
{ {
} }
Wt::Dbo::collection<Cluster::pointer>
Cluster::getAll(Wt::Dbo::Session& session)
{
return session.find<Cluster>();
}
Cluster::pointer Cluster::pointer
Cluster::get(Wt::Dbo::Session& session, std::string type, std::string name) Cluster::create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name)
{
// TODO use like search
return session.find<Cluster>().where("type = ?").where("name = ?").bind( std::string(type, 0, _maxTypeLength)).bind( std::string(name, 0, _maxNameLength));
}
std::vector<Cluster::pointer>
Cluster::getByType(Wt::Dbo::Session& session, std::string type)
{
Wt::Dbo::collection<pointer> res = session.find<Cluster>().where("type = ?").bind( std::string(type, 0, _maxTypeLength)).orderBy("name");
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
Cluster::pointer
Cluster::create(Wt::Dbo::Session& session, std::string type, std::string name)
{ {
return session.add(new Cluster(type, name)); return session.add(new Cluster(type, name));
} }
void std::vector<Cluster::pointer>
Cluster::remove(Wt::Dbo::Session& session, std::string type) Cluster::getAll(Wt::Dbo::Session& session)
{ {
Wt::Dbo::Transaction transaction(session); Wt::Dbo::collection<Cluster::pointer> res = session.find<Cluster>();
session.execute("DELETE FROM cluster WHERE type = ?").bind(type);
return std::vector<Cluster::pointer>(res.begin(), res.end());
} }
Wt::Dbo::Query<Cluster::pointer> Wt::Dbo::Query<Cluster::pointer>
@@ -278,15 +258,6 @@ Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
return query; return query;
} }
std::vector<std::string>
Cluster::getAllTypes(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<std::string> res
= session.query<std::string>("SELECT type from cluster").groupBy("type").orderBy("type");
return std::vector<std::string>(res.begin(), res.end());
}
std::vector<Cluster::pointer> std::vector<Cluster::pointer>
Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size) Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
{ {
@@ -295,6 +266,56 @@ Cluster::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
return std::vector<pointer>(res.begin(), res.end()); return std::vector<pointer>(res.begin(), res.end());
} }
ClusterType::ClusterType(std::string name)
: _name(name)
{
}
ClusterType::pointer
ClusterType::getByName(Wt::Dbo::Session& session, std::string name)
{
return session.find<ClusterType>().where("name = ?").bind(name);
}
std::vector<ClusterType::pointer>
ClusterType::getAll(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<pointer> res = session.find<ClusterType>();
return std::vector<pointer>(res.begin(), res.end());
}
ClusterType::pointer
ClusterType::create(Wt::Dbo::Session& session, std::string name)
{
return session.add(new ClusterType(name));
}
Cluster::pointer
ClusterType::getCluster(std::string name) const
{
assert(self());
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
assert(session());
return session()->find<Cluster>()
.where("name = ?").bind(name)
.where("cluster_type_id = ").bind(self()->id());
}
std::vector<Cluster::pointer>
ClusterType::getClusters() const
{
assert(self());
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
assert(session());
Wt::Dbo::collection<Cluster::pointer> res = session()->find<Cluster>()
.where("cluster_type_id = ").bind(self()->id())
.orderBy("name");
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
} // namespace Database } // namespace Database
+53 -28
View File
@@ -40,64 +40,89 @@ class Artist;
class Release; class Release;
class Track; class Track;
class PlaylistEntry; class PlaylistEntry;
class ClusterType;
class Cluster class Cluster : public Wt::Dbo::Dbo<Cluster>
{ {
public: public:
enum class Type
{
Genre = 1,
Mood = 2,
};
typedef Wt::Dbo::ptr<Cluster> pointer; typedef Wt::Dbo::ptr<Cluster> pointer;
typedef Wt::Dbo::dbo_traits<Cluster>::IdType id_type; typedef Wt::Dbo::dbo_traits<Cluster>::IdType id_type;
Cluster(); Cluster();
Cluster(std::string type, std::string name); Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name);
// Find utility // Find utility
static pointer get(Wt::Dbo::Session& session, std::string type, std::string name);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
static Wt::Dbo::collection<pointer> getAll(Wt::Dbo::Session& session); static std::vector<pointer> getAll(Wt::Dbo::Session& session);
static std::vector<std::string> getAllTypes(Wt::Dbo::Session& session);
static std::vector<pointer> getByType(Wt::Dbo::Session& session, std::string type);
// Create utility // Create utility
static pointer create(Wt::Dbo::Session& session, std::string type, std::string name); static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name);
// Remove utility
static void remove(Wt::Dbo::Session& session, std::string type); // nested transaction
// Accessors // Accessors
const std::string& getName(void) const { return _name; } const std::string& getName(void) const { return _name; }
const std::string& getType(void) const { return _type; } Wt::Dbo::ptr<ClusterType> getType() const { return _clusterType; }
const Wt::Dbo::collection< Wt::Dbo::ptr<Track> >& getTracks() const { return _tracks;} const Wt::Dbo::collection<Wt::Dbo::ptr<Track>>& getTracks() const { return _tracks; }
void addTrack(Wt::Dbo::Session& session, Wt::Dbo::dbo_traits<Track>::IdType trackId);
void addTrack(Wt::Dbo::ptr<Track> track) { _tracks.insert(track); } void addTrack(Wt::Dbo::ptr<Track> track) { _tracks.insert(track); }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
} Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private: private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter); static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128; static const std::size_t _maxNameLength = 128;
static const std::size_t _maxTypeLength = 128;
std::string _type;
std::string _name; std::string _name;
Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks; Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
}; };
class ClusterType : public Wt::Dbo::Dbo<ClusterType>
{
public:
using pointer = Wt::Dbo::ptr<ClusterType>;
using id_type = Wt::Dbo::dbo_traits<ClusterType>::IdType;
ClusterType() {}
ClusterType(std::string name);
static pointer getByName(Wt::Dbo::Session& session, std::string name);
static std::vector<pointer> getAll(Wt::Dbo::Session& session);
static pointer create(Wt::Dbo::Session& session, std::string name);
static void remove(Wt::Dbo::Session& session, std::string name);
// Accessors
const std::string& getName(void) const { return _name; }
std::vector<Cluster::pointer> getClusters() const;
Cluster::pointer getCluster(std::string name) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
}
private:
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
};
class Track class Track
{ {
public: public:
@@ -26,11 +26,15 @@
namespace Database { namespace Database {
static Cluster::pointer getCluster(std::string type, std::string value) static Cluster::pointer getCluster(std::string type, std::string name)
{ {
Cluster::pointer cluster = ( Cluster::get(UpdaterDboSession(), type, value) ); ClusterType::pointer clusterType = ClusterType::getByName(UpdaterDboSession(), type);
if (!clusterType)
clusterType = ClusterType::create(UpdaterDboSession(), type);
auto cluster = clusterType->getCluster(name);
if (!cluster) if (!cluster)
cluster = Cluster::create(UpdaterDboSession(), type, value); cluster = Cluster::create(UpdaterDboSession(), clusterType, name);
return cluster; return cluster;
} }
@@ -196,7 +200,7 @@ HighLevelCluster::handleFilesUpdated(void)
for (auto cluster : clusters) for (auto cluster : clusters)
{ {
// Check if removed // Check if removed
if (cluster->getType() != "high_level") if (cluster->getType()->getName() != "high_level")
continue; continue;
auto it = std::find(newClusterNames.begin(), newClusterNames.end(), cluster->getName()); auto it = std::find(newClusterNames.begin(), newClusterNames.end(), cluster->getName());
+9 -10
View File
@@ -29,9 +29,9 @@
#include "image/Image.hpp" #include "image/Image.hpp"
#include "feature/FeatureExtractor.hpp" #include "feature/FeatureExtractor.hpp"
#include "database/updater/DatabaseUpdater.hpp" #include "scanner/MediaScanner.hpp"
#include "database/updater/DatabaseFeatureExtractor.hpp" //#include "database/updater/DatabaseFeatureExtractor.hpp"
#include "database/updater/DatabaseHighLevelCluster.hpp" //#include "database/updater/DatabaseHighLevelCluster.hpp"
#include "ui/LmsApplication.hpp" #include "ui/LmsApplication.hpp"
@@ -125,20 +125,19 @@ int main(int argc, char* argv[])
std::unique_ptr<Wt::Dbo::SqlConnectionPool> std::unique_ptr<Wt::Dbo::SqlConnectionPool>
connectionPool( Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db")); connectionPool( Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"));
Database::Updater& dbUpdater = Database::Updater::instance(); Scanner::MediaScanner scanner(*connectionPool);
dbUpdater.setConnectionPool(*connectionPool);
// Instanciate the updater's event handler. Order is important // Instanciate the updater's event handler. Order is important
dbUpdater.registerEventHandler(std::make_shared<Database::FeatureExtractor>()); // dbUpdater.registerEventHandler(std::make_shared<Database::FeatureExtractor>());
dbUpdater.registerEventHandler(std::make_shared<Database::HighLevelCluster>()); // dbUpdater.registerEventHandler(std::make_shared<Database::HighLevelCluster>());
// bind entry point // bind entry point
server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create, server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create,
_1, boost::ref(*connectionPool))); _1, boost::ref(*connectionPool)));
// Start // Start
LMS_LOG(MAIN, INFO) << "Starting database updater..."; LMS_LOG(MAIN, INFO) << "Starting Media scanner...";
dbUpdater.start(); scanner.start();
LMS_LOG(MAIN, INFO) << "Starting server..."; LMS_LOG(MAIN, INFO) << "Starting server...";
server.start(); server.start();
@@ -152,7 +151,7 @@ int main(int argc, char* argv[])
server.stop(); server.stop();
LMS_LOG(MAIN, INFO) << "Stopping database updater..."; LMS_LOG(MAIN, INFO) << "Stopping database updater...";
dbUpdater.stop(); scanner.stop();
res = EXIT_SUCCESS; res = EXIT_SUCCESS;
} }
+67 -82
View File
@@ -30,19 +30,23 @@
namespace MetaData namespace MetaData
{ {
bool AvFormat::AvFormat(const std::map<std::string, std::string>& clusterMap)
AvFormat::parse(const boost::filesystem::path& p, Items& items) : _clusterMap(clusterMap)
{ {
}
boost::optional<Items>
AvFormat::parse(const boost::filesystem::path& p)
{
Items items;
Av::MediaFile mediaFile(p); Av::MediaFile mediaFile(p);
if (!mediaFile.open()) if (!mediaFile.open())
return false; return boost::none;
if (!mediaFile.scan()) if (!mediaFile.scan())
return false; return boost::none;
std::map<std::string, std::string> metadata = mediaFile.getMetaData();
// Stream info // Stream info
{ {
@@ -63,41 +67,6 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
items.insert( std::make_pair(MetaData::Type::AudioStreams, audioStreams)); items.insert( std::make_pair(MetaData::Type::AudioStreams, audioStreams));
} }
{
std::vector<VideoStream> videoStreams;
std::vector<Av::Stream> streams = mediaFile.getStreams(Av::Stream::Type::Video);
for (Av::Stream& stream : streams)
{
VideoStream videoStream;
videoStream.desc = stream.desc;
videoStream.bitRate = stream.bitrate;
videoStreams.push_back(videoStream);
}
if (!videoStreams.empty())
items.insert( std::make_pair(MetaData::Type::VideoStreams, videoStreams));
}
{
std::vector<SubtitleStream> subtitleStreams;
std::vector<Av::Stream> streams = mediaFile.getStreams(Av::Stream::Type::Subtitle);
for (Av::Stream& stream : streams)
{
SubtitleStream subtitleStream;
subtitleStream.desc = stream.desc;
subtitleStreams.push_back(subtitleStream);
}
if (!subtitleStreams.empty())
items.insert( std::make_pair(MetaData::Type::SubtitleStreams, subtitleStreams));
}
// Duration // Duration
items.insert( std::make_pair(MetaData::Type::Duration, mediaFile.getDuration() )); items.insert( std::make_pair(MetaData::Type::Duration, mediaFile.getDuration() ));
@@ -106,19 +75,27 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
// Embedded MetaData // Embedded MetaData
// Make sure to convert strings into UTF-8 // Make sure to convert strings into UTF-8
std::map<std::string, std::string>::const_iterator it;
for (it = metadata.begin(); it != metadata.end(); ++it) MetaData::Clusters clusters;
std::map<std::string, std::string> metadataMap = mediaFile.getMetaData();
for (auto metadata : metadataMap)
{ {
if (boost::iequals(it->first, "artist")) const std::string tag = boost::to_upper_copy<std::string>(metadata.first);
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) )); const std::string value = metadata.second;
else if (boost::iequals(it->first, "album")) #if 0
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) )); std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
else if (boost::iequals(it->first, "title")) #endif
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) )); if (tag == "ARTIST")
else if (boost::iequals(it->first, "track")) items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(value)) ));
else if (tag == "ALBUM")
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(value)) ));
else if (tag == "TITLE")
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(value)) ));
else if (tag == "TRACK")
{ {
// Expecting 'Number/Total' // Expecting 'Number/Total'
auto strings = splitString(it->second, "/"); auto strings = splitString(value, "/");
if (strings.size() > 0) if (strings.size() > 0)
{ {
@@ -134,10 +111,10 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
} }
} }
} }
else if (boost::iequals(it->first, "disc")) else if (tag == "DISC")
{ {
// Expecting 'Number/Total' // Expecting 'Number/Total'
auto strings = splitString(it->second, "/"); auto strings = splitString(value, "/");
if (strings.size() > 0) if (strings.size() > 0)
{ {
@@ -153,49 +130,57 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
} }
} }
} }
else if (boost::iequals(it->first, "date") else if (tag == "DATE"
|| boost::iequals(it->first, "year") || tag == "YEAR"
|| boost::iequals(it->first, "WM/Year")) || tag == "WM/Year")
{ {
boost::posix_time::ptime p; boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p)) if (readAsPosixTime(value, p))
items.insert( std::make_pair(MetaData::Type::Date, p)); items.insert( std::make_pair(MetaData::Type::Date, p));
} }
else if (boost::iequals(it->first, "TDOR") // Original release time (ID3v2 2.4) else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|| boost::iequals(it->first, "TORY")) // Original release year || tag == "TORY") // Original release year
{ {
boost::posix_time::ptime p; boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p)) if (readAsPosixTime(value, p))
items.insert( std::make_pair(MetaData::Type::OriginalDate, p)); items.insert( std::make_pair(MetaData::Type::OriginalDate, p));
} }
else if (boost::iequals(it->first, "genre")) else if (tag == "MUSICBRAINZ ARTIST ID"
|| tag == "MUSICBRAINZ_ARTISTID")
{ {
// TODO use splitStrings items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(value)) ));
std::list<std::string> genres; }
if (readList(it->second, ";,\\", genres)) else if (tag == "MUSICBRAINZ ALBUM ID"
items.insert( std::make_pair(MetaData::Type::Genres, genres)); || tag == "MUSICBRAINZ_ALBUMID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(value)) ));
}
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ_TRACKID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( stringToUTF8(value)) ));
}
else if (tag == "ACOUSTID ID")
{
items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim( stringToUTF8(value)) ));
}
else if (_clusterMap.find(tag) != _clusterMap.end())
{
std::vector<std::string> clusterNames = splitString(value, ";,\\");
} if (!clusterNames.empty())
else if (boost::iequals(it->first, "MusicBrainz Artist Id") {
|| boost::iequals(it->first, "MUSICBRAINZ_ARTISTID")) clusters[_clusterMap[tag]] = std::set<std::string>(clusterNames.begin(), clusterNames.end());
{ }
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(it->second)) ));
}
else if (boost::iequals(it->first, "MusicBrainz Album Id")
|| boost::iequals(it->first, "MUSICBRAINZ_ALBUMID"))
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(it->second)) ));
}
else if (boost::iequals(it->first, "MusicBrainz Release Track Id")
|| boost::iequals(it->first, "MUSICBRAINZ_RELEASETRACKID")
|| boost::iequals(it->first, "MUSICBRAINZ_TRACKID"))
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( stringToUTF8(it->second)) ));
} }
} }
return true; if (!clusters.empty())
items.insert( std::make_pair(MetaData::Type::Clusters, clusters) );
return items;
} }
} // namespace MetaData } // namespace MetaData
+14 -6
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Emeric Poupon * Copyright (C) 2018 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,8 +17,10 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef METADATA_AVFORMAT_HPP #pragma once
#define METADATA_AVFORMAT_HPP
#include <map>
#include <string>
#include "MetaData.hpp" #include "MetaData.hpp"
@@ -30,13 +32,19 @@ class AvFormat : public Parser
{ {
public: public:
bool parse(const boost::filesystem::path& p, Items& items); AvFormat(const std::map<std::string, std::string>& clusterMap
= {
{"GENRE", "Genre" },
{"ALBUMGROUPING", "Group" }
});
boost::optional<Items> parse(const boost::filesystem::path& p);
private: private:
std::map<std::string,std::string> _clusterMap;
}; };
} // namespace MetaData } // namespace MetaData
#endif
+8 -18
View File
@@ -21,8 +21,10 @@
#define METADATA_HPP #define METADATA_HPP
#include <map> #include <map>
#include <set>
#include <boost/any.hpp> #include <boost/any.hpp>
#include <boost/optional.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
namespace MetaData namespace MetaData
@@ -30,10 +32,11 @@ namespace MetaData
enum class Type enum class Type
{ {
// Name Type of the value
Artist, // string Artist, // string
Title, // string Title, // string
Album, // string Album, // string
Genres, // list<string> Clusters, // Clusters, ex: { "genre", {"death metal", "brutal death"} }, { "albumgrouping", {"metal"} }
Duration, // boost::posix_time::time_duration Duration, // boost::posix_time::time_duration
TrackNumber, // size_t TrackNumber, // size_t
DiscNumber, // size_t DiscNumber, // size_t
@@ -43,12 +46,11 @@ namespace MetaData
OriginalDate, // boost::posix_time::ptime OriginalDate, // boost::posix_time::ptime
HasCover, // bool HasCover, // bool
AudioStreams, // vector<AudioStream> AudioStreams, // vector<AudioStream>
VideoStreams, // vector<VideoStream>
SubtitleStreams, // vector<SubtitleStream>
MusicBrainzArtistID, // string MusicBrainzArtistID, // string
MusicBrainzAlbumID, // string MusicBrainzAlbumID, // string
MusicBrainzTrackID, // string MusicBrainzTrackID, // string
MusicBrainzRecordingID, // string MusicBrainzRecordingID, // string
AcoustID, // string
}; };
// Used by Streams // Used by Streams
@@ -58,28 +60,16 @@ namespace MetaData
std::size_t bitRate; std::size_t bitRate;
}; };
struct VideoStream
{
std::string desc;
std::size_t bitRate;
};
struct SubtitleStream
{
std::string desc;
};
// Type and associated data // Type and associated data
// See enum Type's comments // See enum Type's comments
typedef std::map<Type, boost::any> Items; using Items = std::map<Type, boost::any>;
using Clusters = std::map<std::string, std::set<std::string>>;
class Parser class Parser
{ {
public: public:
typedef std::shared_ptr<Parser> pointer; virtual boost::optional<Items> parse(const boost::filesystem::path& p) = 0;
virtual bool parse(const boost::filesystem::path& p, Items& items) = 0;
}; };
+45 -19
View File
@@ -32,18 +32,25 @@
namespace MetaData namespace MetaData
{ {
bool TagLibParser::TagLibParser(const std::map<std::string, std::string>& clusterMap)
TagLibParser::parse(const boost::filesystem::path& p, Items& items) : _clusterMap(clusterMap)
{
}
boost::optional<Items>
TagLibParser::parse(const boost::filesystem::path& p)
{ {
TagLib::FileRef f(p.string().c_str(), TagLib::FileRef f(p.string().c_str(),
true, // read audio properties true, // read audio properties
TagLib::AudioProperties::Average); TagLib::AudioProperties::Average);
if (f.isNull()) if (f.isNull())
return false; return boost::none;
if (!f.audioProperties()) if (!f.audioProperties())
return false; return boost::none;
Items items;
{ {
TagLib::AudioProperties *properties = f.audioProperties(); TagLib::AudioProperties *properties = f.audioProperties();
@@ -70,32 +77,46 @@ TagLibParser::parse(const boost::filesystem::path& p, Items& items)
if (f.tag()) if (f.tag())
{ {
TagLib::PropertyMap tags = f.file()->properties(); MetaData::Clusters clusters;
TagLib::PropertyMap properties = f.file()->properties();
for(TagLib::PropertyMap::ConstIterator itElem = tags.begin(); itElem != tags.end(); ++itElem) for(auto property : properties)
{ {
const std::string tag = itElem->first.to8Bit(true); const std::string tag = property.first.upper().to8Bit(true);
const TagLib::StringList &values = itElem->second; const TagLib::StringList& values = property.second;
if (tag.empty() || values.isEmpty() || values.front().isEmpty()) if (tag.empty() || values.isEmpty() || values.front().isEmpty())
continue; continue;
// TODO validate MBID format // TODO validate MBID format
#if 0
std::cout << "TAG = '" << tag << "'" << std::endl;
for (auto value : values)
{
std::cout << "\t'" << value.to8Bit(true) << "'" << std::endl;
}
#endif
if (tag == "ARTIST") if (tag == "ARTIST")
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( values.front().to8Bit(true))));
else if (tag == "ALBUM") else if (tag == "ALBUM")
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::Album, stringTrim( values.front().to8Bit(true))));
else if (tag == "TITLE") else if (tag == "TITLE")
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::Title, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_RELEASETRACKID") else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( values.front().to8Bit(true))));
}
else if (tag == "MUSICBRAINZ_ARTISTID") else if (tag == "MUSICBRAINZ_ARTISTID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_ALBUMID") else if (tag == "MUSICBRAINZ_ALBUMID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( values.front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_TRACKID") else if (tag == "MUSICBRAINZ_TRACKID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzRecordingID, stringTrim( tags["MUSICBRAINZ_TRACKID"].front().to8Bit(true)))); items.insert( std::make_pair(MetaData::Type::MusicBrainzRecordingID, stringTrim( values.front().to8Bit(true))));
else if (tag == "ACOUSTID_ID")
items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim( values.front().to8Bit(true))));
else if (tag == "TRACKNUMBER") else if (tag == "TRACKNUMBER")
{ {
// Expecting 'Number/Total' // Expecting 'Number/Total'
@@ -160,24 +181,29 @@ TagLibParser::parse(const boost::filesystem::path& p, Items& items)
items.insert( std::make_pair(MetaData::Type::OriginalDate, p)); items.insert( std::make_pair(MetaData::Type::OriginalDate, p));
} }
} }
else if (tag == "GENRE")
{
std::list<std::string> genreList;
for (TagLib::StringList::ConstIterator itGenre = values.begin(); itGenre != values.end(); ++itGenre)
genreList.push_back(itGenre->to8Bit(true));
items.insert( std::make_pair(MetaData::Type::Genres, genreList) );
}
else if (tag == "METADATA_BLOCK_PICTURE") else if (tag == "METADATA_BLOCK_PICTURE")
{ {
// Only add once // Only add once
if (items.find(MetaData::Type::HasCover) == items.end()) if (items.find(MetaData::Type::HasCover) == items.end())
items.insert( std::make_pair(MetaData::Type::HasCover, true)); items.insert( std::make_pair(MetaData::Type::HasCover, true));
} }
// Check if a hit a cluster tag
else if (_clusterMap.find(tag) != _clusterMap.end())
{
std::set<std::string> clusterNames;
for (const auto& value : values)
clusterNames.insert(value.to8Bit(true));
if (!clusterNames.empty())
clusters[_clusterMap[tag]] = clusterNames;
}
} }
if (!clusters.empty())
items.insert( std::make_pair(MetaData::Type::Clusters, clusters) );
} }
return true; return items;
} }
} // namespace MetaData } // namespace MetaData
+14 -2
View File
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2016 Emeric Poupon * Copyright (C) 2018 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,6 +19,9 @@
#pragma once #pragma once
#include <map>
#include <string>
#include "MetaData.hpp" #include "MetaData.hpp"
namespace MetaData namespace MetaData
@@ -28,10 +31,19 @@ namespace MetaData
class TagLibParser : public Parser class TagLibParser : public Parser
{ {
public: public:
bool parse(const boost::filesystem::path& p, Items& items);
// Provide a map for TagLib name -> Cluster name
TagLibParser(const std::map<std::string, std::string>& clusterMap
= {
{"GENRE", "Genre" },
{"ALBUMGROUPING", "Group" }
});
boost::optional<Items> parse(const boost::filesystem::path& p);
private: private:
std::map<std::string,std::string> _clusterMap;
}; };
} // namespace MetaData } // namespace MetaData
@@ -32,8 +32,7 @@
#include "utils/Path.hpp" #include "utils/Path.hpp"
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
#include "MediaScanner.hpp"
#include "DatabaseUpdater.hpp"
namespace { namespace {
@@ -85,20 +84,6 @@ isFileSupported(const boost::filesystem::path& file, const std::vector<boost::fi
return false; return false;
} }
std::vector<boost::filesystem::path>
getRootDirectoriesByType(Wt::Dbo::Session& session, Database::MediaDirectory::Type type)
{
Wt::Dbo::Transaction transaction(session);
std::vector<Database::MediaDirectory::pointer> rootDirs = Database::MediaDirectory::getByType(session, type);
std::vector<boost::filesystem::path> res;
for (auto rootDir : rootDirs)
res.push_back(rootDir->getPath());
return res;
}
bool bool
isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath) isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath)
{ {
@@ -118,40 +103,33 @@ isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem:
} // namespace } // namespace
namespace Database { namespace Scanner {
Updater& Updater::instance(void) using namespace Database;
{
static Updater updater;
return updater;
}
Updater::Updater() MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
: _running(true), : _running(false),
_scheduleTimer(_ioService) _scheduleTimer(_ioService),
_db(connectionPool)
{ {
_ioService.setThreadCount(1); _ioService.setThreadCount(1);
Wt::Dbo::Transaction transaction(_db.getSession());
if (!Setting::exists(_db.getSession(), "file_extensions"))
Setting::setString(_db.getSession(), "file_extensions", ".mp3 .ogg .oga .aac .m4a .flac .wav .wma .aif .aiff .ape .mpc .shn" );
} }
void void
Updater::setConnectionPool(Wt::Dbo::SqlConnectionPool& connectionPool) MediaScanner::restart(void)
{
_db = new Database::Handler(connectionPool);
}
void
Updater::restart(void)
{ {
stop(); stop();
start(); start();
} }
void void
Updater::start(void) MediaScanner::start(void)
{ {
if (_db == nullptr)
throw std::logic_error("uninitialized db!");
_running = true; _running = true;
// post some jobs in the io_service // post some jobs in the io_service
@@ -161,7 +139,7 @@ Updater::start(void)
} }
void void
Updater::stop(void) MediaScanner::stop(void)
{ {
_running = false; _running = false;
@@ -171,9 +149,9 @@ Updater::stop(void)
} }
void void
Updater::processNextJob(void) MediaScanner::processNextJob(void)
{ {
if (Setting::getBool(_db->getSession(), "manual_scan_requested", false)) if (Setting::getBool(_db.getSession(), "manual_scan_requested", false))
{ {
LMS_LOG(DBUPDATER, INFO) << "Manual scan requested!"; LMS_LOG(DBUPDATER, INFO) << "Manual scan requested!";
scheduleScan( boost::posix_time::seconds(0) ); scheduleScan( boost::posix_time::seconds(0) );
@@ -181,11 +159,11 @@ Updater::processNextJob(void)
else else
{ {
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
boost::posix_time::time_duration startTime = Setting::getDuration(_db->getSession(), "update_start_time"); boost::posix_time::time_duration startTime = Setting::getDuration(_db.getSession(), "update_start_time");
boost::gregorian::date nextScanDate; boost::gregorian::date nextScanDate;
std::string updatePeriod = Setting::getString(_db->getSession(), "update_period", "never"); std::string updatePeriod = Setting::getString(_db.getSession(), "update_period", "never");
if (updatePeriod == "daily") if (updatePeriod == "daily")
{ {
if (now.time_of_day() < startTime) if (now.time_of_day() < startTime)
@@ -214,72 +192,58 @@ Updater::processNextJob(void)
} }
void void
Updater::scheduleScan( boost::posix_time::time_duration duration) MediaScanner::scheduleScan( boost::posix_time::time_duration duration)
{ {
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration; LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration;
_scheduleTimer.expires_from_now(duration); _scheduleTimer.expires_from_now(duration);
_scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) ); _scheduleTimer.async_wait( boost::bind( &MediaScanner::process, this, boost::asio::placeholders::error) );
} }
void void
Updater::scheduleScan( boost::posix_time::ptime time) MediaScanner::scheduleScan( boost::posix_time::ptime time)
{ {
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << time; LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << time;
_scheduleTimer.expires_at(time); _scheduleTimer.expires_at(time);
_scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) ); _scheduleTimer.async_wait( boost::bind( &MediaScanner::process, this, boost::asio::placeholders::error) );
} }
void void
Updater::process(boost::system::error_code err) MediaScanner::process(boost::system::error_code err)
{ {
if (err) if (err)
return; return;
updateFileExtensions(); refreshScanSettings();
Stats stats; Stats stats;
checkAudioFiles(stats); checkAudioFiles(stats);
std::vector<RootDirectory> rootDirectories; for (auto rootDirectory : _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) if (!_running)
break; break;
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'..."; LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory << "'...";
processRootDirectory(rootDirectory, stats); processRootDirectory(rootDirectory, stats);
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE"; LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory << "' DONE";
} }
if (_running) if (_running)
{
checkDuplicatedAudioFiles(stats); checkDuplicatedAudioFiles(stats);
LMS_LOG(DBUPDATER, INFO) << "Processed all files, now calling listeners...";
for (auto eventHandler : _eventHandlers)
eventHandler->handleFilesUpdated();
}
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.nbAdded << ", removed = " << stats.nbRemoved << ", updated = " << stats.nbUpdated << "), Not changed = " << stats.nbNoChange << ", Scanned = " << stats.nbScanned << " (errors = " << stats.nbScanErrors << ", not imported = " << stats.nbNotImported << ")"; LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.nbAdded << ", removed = " << stats.nbRemoved << ", updated = " << stats.nbUpdated << "), Not changed = " << stats.nbNoChange << ", Scanned = " << stats.nbScanned << " (errors = " << stats.nbScanErrors << ", not imported = " << stats.nbNotImported << ")";
// Update database stats // Update database stats
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
if (stats.nbChanges() > 0) if (stats.nbChanges() > 0)
Setting::setTime(_db->getSession(), "last_update", now); Setting::setTime(_db.getSession(), "last_update", now);
// Save the last scan only if it has been completed // Save the last scan only if it has been completed
if (_running) if (_running)
{ {
Setting::setTime(_db->getSession(), "last_scan", now); Setting::setTime(_db.getSession(), "last_scan", now);
Setting::setBool(_db->getSession(), "manual_scan_requested", false); Setting::setBool(_db.getSession(), "manual_scan_requested", false);
processNextJob(); processNextJob();
@@ -288,26 +252,30 @@ Updater::process(boost::system::error_code err)
} }
void void
Updater::updateFileExtensions() MediaScanner::refreshScanSettings()
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
_audioFileExtensions.clear(); _fileExtensions.clear();
for (auto extension : splitString(Setting::getString(_db->getSession(), "audio_file_extensions"), " ")) for (auto extension : splitString(Setting::getString(_db.getSession(), "file_extensions"), " "))
_audioFileExtensions.push_back( extension ); _fileExtensions.push_back( extension );
_rootDirectories.clear();
for (auto rootDir : Database::MediaDirectory::getAll(_db.getSession()))
_rootDirectories.push_back(rootDir->getPath());
} }
Artist::pointer Artist::pointer
Updater::getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& mbid) MediaScanner::getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& mbid)
{ {
Artist::pointer artist; Artist::pointer artist;
// First try to get by MBID // First try to get by MBID
if (!mbid.empty()) if (!mbid.empty())
{ {
artist = Artist::getByMBID( _db->getSession(), mbid ); artist = Artist::getByMBID( _db.getSession(), mbid );
if (!artist) if (!artist)
artist = Artist::create( _db->getSession(), name, mbid); artist = Artist::create( _db.getSession(), name, mbid);
return artist; return artist;
} }
@@ -315,7 +283,7 @@ Updater::getArtist( const boost::filesystem::path& file, const std::string& name
// Fall back on artist name (collisions may occur) // Fall back on artist name (collisions may occur)
if (!name.empty()) if (!name.empty())
{ {
for (Artist::pointer sameNamedArtist : Artist::getByName( _db->getSession(), name )) for (Artist::pointer sameNamedArtist : Artist::getByName( _db.getSession(), name ))
{ {
if (sameNamedArtist->getMBID().empty()) if (sameNamedArtist->getMBID().empty())
{ {
@@ -326,7 +294,7 @@ Updater::getArtist( const boost::filesystem::path& file, const std::string& name
// No Artist found with the same name and without MBID -> creating // No Artist found with the same name and without MBID -> creating
if (!artist) if (!artist)
artist = Artist::create( _db->getSession(), name); artist = Artist::create( _db.getSession(), name);
return artist; return artist;
} }
@@ -335,16 +303,16 @@ Updater::getArtist( const boost::filesystem::path& file, const std::string& name
} }
Release::pointer Release::pointer
Updater::getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& mbid) MediaScanner::getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& mbid)
{ {
Release::pointer release; Release::pointer release;
// First try to get by MBID // First try to get by MBID
if (!mbid.empty()) if (!mbid.empty())
{ {
release = Release::getByMBID( _db->getSession(), mbid ); release = Release::getByMBID( _db.getSession(), mbid );
if (!release) if (!release)
release = Release::create( _db->getSession(), name, mbid); release = Release::create( _db.getSession(), name, mbid);
return release; return release;
} }
@@ -352,7 +320,7 @@ Updater::getRelease( const boost::filesystem::path& file, const std::string& nam
// Fall back on release name (collisions may occur) // Fall back on release name (collisions may occur)
if (!name.empty()) if (!name.empty())
{ {
for (Release::pointer sameNamedRelease : Release::getByName( _db->getSession(), name )) for (Release::pointer sameNamedRelease : Release::getByName( _db.getSession(), name ))
{ {
if (sameNamedRelease->getMBID().empty()) if (sameNamedRelease->getMBID().empty())
{ {
@@ -363,7 +331,7 @@ Updater::getRelease( const boost::filesystem::path& file, const std::string& nam
// No release found with the same name and without MBID -> creating // No release found with the same name and without MBID -> creating
if (!release) if (!release)
release = Release::create( _db->getSession(), name); release = Release::create( _db.getSession(), name);
return release; return release;
} }
@@ -372,32 +340,39 @@ Updater::getRelease( const boost::filesystem::path& file, const std::string& nam
} }
std::vector<Cluster::pointer> std::vector<Cluster::pointer>
Updater::getGenreClusters( const std::list<std::string>& names) MediaScanner::getClusters( const MetaData::Clusters& clustersNames)
{ {
std::vector< Cluster::pointer > genres; std::vector< Cluster::pointer > clusters;
for (const std::string& name : names) for (auto clusterNames : clustersNames)
{ {
Cluster::pointer genre ( Cluster::get(_db->getSession(), "Genre", name) ); ClusterType::pointer clusterType = ClusterType::getByName(_db.getSession(), clusterNames.first);
if (!genre) if (!clusterType)
genre = Cluster::create(_db->getSession(), "Genre", name); clusterType = ClusterType::create(_db.getSession(), clusterNames.first);
genres.push_back( genre ); for (auto clusterName : clusterNames.second)
{
Cluster::pointer cluster = clusterType->getCluster(clusterName);
if (!cluster)
cluster = Cluster::create(_db.getSession(), clusterType, clusterName);
clusters.push_back(cluster);
}
} }
return genres; return clusters;
} }
void void
Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) MediaScanner::processAudioFile( const boost::filesystem::path& file, Stats& stats)
{ {
boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) ); boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) );
// Skip file if last write is the same // Skip file if last write is the same
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db->getSession(), file); Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
if (track && track->getLastWriteTime() == lastWriteTime) if (track && track->getLastWriteTime() == lastWriteTime)
{ {
@@ -406,8 +381,8 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
} }
} }
MetaData::Items items; boost::optional<MetaData::Items> items = _metadataParser.parse(file);
if (!_metadataParser.parse(file, items)) if (!items)
{ {
stats.nbScanErrors++; stats.nbScanErrors++;
return; return;
@@ -418,15 +393,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::vector<unsigned char> checksum ; std::vector<unsigned char> checksum ;
computeCrc(file, checksum); computeCrc(file, checksum);
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db->getSession(), file); Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
// We estimate this is a audio file if: // We estimate this is a audio file if:
// - we found a least one audio stream // - we found a least one audio stream
// - the duration is not null // - the duration is not null
if (items.find(MetaData::Type::AudioStreams) == items.end() if ((*items).find(MetaData::Type::AudioStreams) == (*items).end()
|| boost::any_cast<std::vector<MetaData::AudioStream> >(items[MetaData::Type::AudioStreams]).empty()) || boost::any_cast<std::vector<MetaData::AudioStream>> ((*items)[MetaData::Type::AudioStreams]).empty())
{ {
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no audio stream found)"; LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no audio stream found)";
@@ -439,8 +414,8 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
stats.nbNotImported++; stats.nbNotImported++;
return; return;
} }
if (items.find(MetaData::Type::Duration) == items.end() if ((*items).find(MetaData::Type::Duration) == (*items).end()
|| boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]).total_seconds() <= 0) || boost::any_cast<boost::posix_time::time_duration>((*items)[MetaData::Type::Duration]).total_seconds() <= 0)
{ {
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no duration or duration <= 0)"; LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no duration or duration <= 0)";
@@ -456,9 +431,9 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
// ***** Title // ***** Title
std::string title; std::string title;
if (items.find(MetaData::Type::Title) != items.end()) if ((*items).find(MetaData::Type::Title) != (*items).end())
{ {
title = boost::any_cast<std::string>(items[MetaData::Type::Title]); title = boost::any_cast<std::string>((*items)[MetaData::Type::Title]);
} }
else else
{ {
@@ -470,13 +445,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
// ***** Clusters // ***** Clusters
std::vector< Cluster::pointer > genres; std::vector< Cluster::pointer > genres;
{ {
std::list<std::string> genreList; MetaData::Clusters clusterNames;
if (items.find(MetaData::Type::Genres) != items.end()) if ((*items).find(MetaData::Type::Clusters) != (*items).end())
genreList = boost::any_cast< std::list<std::string> > (items[MetaData::Type::Genres]); {
clusterNames = boost::any_cast<MetaData::Clusters> ((*items)[MetaData::Type::Clusters]);
}
// TODO rename // TODO rename
genres = getGenreClusters( genreList ); genres = getClusters( clusterNames );
} }
// ***** Artist // ***** Artist
@@ -485,11 +462,11 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::string artistName; std::string artistName;
std::string artistMusicBrainzID; std::string artistMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzArtistID) != items.end()) if ((*items).find(MetaData::Type::MusicBrainzArtistID) != (*items).end())
artistMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzArtistID] ); artistMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzArtistID] );
if (items.find(MetaData::Type::Artist) != items.end()) if ((*items).find(MetaData::Type::Artist) != (*items).end())
artistName = boost::any_cast<std::string>(items[MetaData::Type::Artist]); artistName = boost::any_cast<std::string>((*items)[MetaData::Type::Artist]);
artist = getArtist(file, artistName, artistMusicBrainzID); artist = getArtist(file, artistName, artistMusicBrainzID);
} }
@@ -500,11 +477,11 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::string releaseName; std::string releaseName;
std::string releaseMusicBrainzID; std::string releaseMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzAlbumID) != items.end()) if ((*items).find(MetaData::Type::MusicBrainzAlbumID) != (*items).end())
releaseMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzAlbumID] ); releaseMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzAlbumID] );
if (items.find(MetaData::Type::Album) != items.end()) if ((*items).find(MetaData::Type::Album) != (*items).end())
releaseName = boost::any_cast<std::string>(items[MetaData::Type::Album]); releaseName = boost::any_cast<std::string>((*items)[MetaData::Type::Album]);
release = getRelease(file, releaseName, releaseMusicBrainzID); release = getRelease(file, releaseName, releaseMusicBrainzID);
} }
@@ -515,7 +492,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
if (!track) if (!track)
{ {
// Create a new song // Create a new song
track = Track::create(_db->getSession(), file); track = Track::create(_db.getSession(), file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'"; LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'";
stats.nbAdded++; stats.nbAdded++;
} }
@@ -537,7 +514,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
track.modify()->setRelease(release); track.modify()->setRelease(release);
track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setDuration( boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]) ); track.modify()->setDuration( boost::any_cast<boost::posix_time::time_duration>((*items)[MetaData::Type::Duration]) );
track.modify()->setAddedTime( boost::posix_time::second_clock::local_time() ); track.modify()->setAddedTime( boost::posix_time::second_clock::local_time() );
{ {
@@ -555,53 +532,54 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
track.modify()->setGenres( trackClusterList ); track.modify()->setGenres( trackClusterList );
} }
if (items.find(MetaData::Type::TrackNumber) != items.end()) if ((*items).find(MetaData::Type::TrackNumber) != (*items).end())
track.modify()->setTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TrackNumber]) ); track.modify()->setTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TrackNumber]) );
if (items.find(MetaData::Type::TotalTrack) != items.end()) if ((*items).find(MetaData::Type::TotalTrack) != (*items).end())
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalTrack]) ); track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalTrack]) );
if (items.find(MetaData::Type::DiscNumber) != items.end()) if ((*items).find(MetaData::Type::DiscNumber) != (*items).end())
track.modify()->setDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::DiscNumber]) ); track.modify()->setDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::DiscNumber]) );
if (items.find(MetaData::Type::TotalDisc) != items.end()) if ((*items).find(MetaData::Type::TotalDisc) != (*items).end())
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalDisc]) ); track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalDisc]) );
if (items.find(MetaData::Type::Date) != items.end()) if ((*items).find(MetaData::Type::Date) != (*items).end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::Date]) ); track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>((*items)[MetaData::Type::Date]) );
if (items.find(MetaData::Type::OriginalDate) != items.end()) if ((*items).find(MetaData::Type::OriginalDate) != (*items).end())
{ {
track.modify()->setOriginalDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) ); track.modify()->setOriginalDate( boost::any_cast<boost::posix_time::ptime>((*items)[MetaData::Type::OriginalDate]) );
// If a file has an OriginalDate but no date, set the date to ease filtering // If a file has an OriginalDate but no date, set the date to ease filtering
if (items.find(MetaData::Type::Date) == items.end()) if ((*items).find(MetaData::Type::Date) == (*items).end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) ); track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>((*items)[MetaData::Type::OriginalDate]) );
} }
if (items.find(MetaData::Type::MusicBrainzRecordingID) != items.end()) if ((*items).find(MetaData::Type::MusicBrainzRecordingID) != (*items).end())
{ {
track.modify()->setMBID( boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzRecordingID]) ); track.modify()->setMBID( boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzRecordingID]) );
} }
if (items.find(MetaData::Type::HasCover) != items.end()) if ((*items).find(MetaData::Type::HasCover) != (*items).end())
{ {
bool hasCover = boost::any_cast<bool>(items[MetaData::Type::HasCover]); bool hasCover = boost::any_cast<bool>((*items)[MetaData::Type::HasCover]);
track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None ); track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None );
} }
transaction.commit(); // TODO check added/modified
_sigAddedTrack.emit(track);
_sigTrackChanged.emit(true, track.id(), track->getMBID(), track->getPath()); transaction.commit();
} }
void void
Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats) MediaScanner::processRootDirectory(boost::filesystem::path rootDirectory, Stats& stats)
{ {
boost::system::error_code ec; boost::system::error_code ec;
boost::filesystem::recursive_directory_iterator itPath(rootDirectory.path, ec); boost::filesystem::recursive_directory_iterator itPath(rootDirectory, ec);
boost::filesystem::recursive_directory_iterator itEnd; boost::filesystem::recursive_directory_iterator itEnd;
while (!ec && itPath != itEnd) while (!ec && itPath != itEnd)
@@ -614,20 +592,15 @@ Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats)
if (boost::filesystem::is_regular(path)) if (boost::filesystem::is_regular(path))
{ {
switch( rootDirectory.type ) if (isFileSupported(path, _fileExtensions))
{ processAudioFile(path, stats );
case Database::MediaDirectory::Audio:
if (isFileSupported(path, _audioFileExtensions))
processAudioFile(path, stats );
break;
}
} }
} }
} }
bool // Check if a file exists and is still in a root directory
Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions) static bool
checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions)
{ {
try try
{ {
@@ -678,12 +651,11 @@ Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::fi
} }
void void
Updater::checkAudioFiles( Stats& stats ) MediaScanner::checkAudioFiles( Stats& stats )
{ {
LMS_LOG(DBUPDATER, INFO) << "Checking audio files..."; LMS_LOG(DBUPDATER, INFO) << "Checking audio files...";
std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db->getSession());; std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db.getSession());;
std::vector<boost::filesystem::path> rootDirs = getRootDirectoriesByType(_db->getSession(), Database::MediaDirectory::Audio);
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (auto& trackPath : trackPaths) for (auto& trackPath : trackPaths)
@@ -691,11 +663,11 @@ Updater::checkAudioFiles( Stats& stats )
if (!_running) if (!_running)
return; return;
if (!checkFile(trackPath, rootDirs, _audioFileExtensions)) if (!checkFile(trackPath, _rootDirectories, _fileExtensions))
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
Track::pointer track = Track::getByPath(_db->getSession(), trackPath); Track::pointer track = Track::getByPath(_db.getSession(), trackPath);
if (track) if (track)
{ {
track.remove(); track.remove();
@@ -706,25 +678,25 @@ Updater::checkAudioFiles( Stats& stats )
LMS_LOG(DBUPDATER, DEBUG) << "Checking Clusters..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking Clusters...";
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
// Now process orphan Cluster (no track) // Now process orphan Cluster (no track)
auto genres = Cluster::getAll(_db->getSession()); auto clusters = Cluster::getAll(_db.getSession());
for (auto genre : genres) for (auto cluster : clusters)
{ {
if (genre->getTracks().size() == 0) if (cluster->getTracks().size() == 0)
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan genre '" << genre->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
genre.remove(); cluster.remove();
} }
} }
} }
LMS_LOG(DBUPDATER, DEBUG) << "Checking artists..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking artists...";
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
auto artists = Artist::getAllOrphans(_db->getSession()); auto artists = Artist::getAllOrphans(_db.getSession());
for (auto artist : artists) for (auto artist : artists)
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
@@ -734,9 +706,9 @@ Updater::checkAudioFiles( Stats& stats )
LMS_LOG(DBUPDATER, DEBUG) << "Checking releases..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking releases...";
{ {
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
auto releases = Release::getAllOrphans(_db->getSession()); auto releases = Release::getAllOrphans(_db.getSession());
for (auto release : releases) for (auto release : releases)
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
@@ -748,19 +720,19 @@ Updater::checkAudioFiles( Stats& stats )
} }
void void
Updater::checkDuplicatedAudioFiles(Stats& stats) MediaScanner::checkDuplicatedAudioFiles(Stats& stats)
{ {
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files"; LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files";
Wt::Dbo::Transaction transaction(_db->getSession()); Wt::Dbo::Transaction transaction(_db.getSession());
std::vector<Track::pointer> tracks = Database::Track::getMBIDDuplicates(_db->getSession()); std::vector<Track::pointer> tracks = Database::Track::getMBIDDuplicates(_db.getSession());
for (Track::pointer track : tracks) for (Track::pointer track : tracks)
{ {
LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID() << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName(); LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID() << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName();
} }
tracks = Database::Track::getChecksumDuplicates(_db->getSession()); tracks = Database::Track::getChecksumDuplicates(_db.getSession());
for (Track::pointer track : tracks) for (Track::pointer track : tracks)
{ {
LMS_LOG(DBUPDATER, INFO) << "Found duplicated checksum [" << bufferToString(track->getChecksum()) << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName(); LMS_LOG(DBUPDATER, INFO) << "Found duplicated checksum [" << bufferToString(track->getChecksum()) << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName();
@@ -771,4 +743,4 @@ Updater::checkDuplicatedAudioFiles(Stats& stats)
} }
} // namespace Database } // namespace Scanner
@@ -19,9 +19,6 @@
#pragma once #pragma once
#include <mutex>
#include <list>
#include <boost/asio/deadline_timer.hpp> #include <boost/asio/deadline_timer.hpp>
#include <Wt/WIOService> #include <Wt/WIOService>
@@ -31,21 +28,14 @@
#include "database/DatabaseHandler.hpp" #include "database/DatabaseHandler.hpp"
namespace Database { namespace Scanner {
class UpdaterEventHandler; class MediaScanner
class Updater
{ {
public: public:
static Updater& instance(); MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
void setConnectionPool(Wt::Dbo::SqlConnectionPool& connectionPool);
void setAudioExtensions(const std::vector<std::string>& extensions);
void setVideoExtensions(const std::vector<std::string>& extensions);
void start(); void start();
void stop(); void stop();
@@ -64,37 +54,21 @@ class Updater
std::size_t nbChanges() const { return nbAdded + nbRemoved + nbUpdated;} std::size_t nbChanges() const { return nbAdded + nbRemoved + nbUpdated;}
}; };
// Emitted when the whole database has been scanned (and all the event handlers have been called)
// Called just after track addition
Wt::Signal<Database::Track::pointer>& addedTrack() { return _sigAddedTrack; }
// Called just before track removal
Wt::Signal<Database::Track::pointer>& removedTrack() { return _sigRemovedTrack; }
// Called just after track modification
Wt::Signal<Database::Track::pointer>& modifiedTrack() { return _sigModifiedTrack; }
// Called just after scan complete
Wt::Signal<Stats>& scanComplete() { return _sigScanComplete; } Wt::Signal<Stats>& scanComplete() { return _sigScanComplete; }
// Emitted when a track changed
// true -> added or modified, false -> to be deleted
// id of the track
// musicbrainz recordid
// path of the track
typedef Wt::Signal<bool, Track::id_type, std::string, boost::filesystem::path> SigTrackChanged;
SigTrackChanged& trackChanged() { return _sigTrackChanged; }
std::mutex& getMutex(void) { return _mutex; }
Database::Handler& getDb(void) { return *_db; }
bool quitRequested(void) const { return !_running;}
void registerEventHandler(std::shared_ptr<UpdaterEventHandler> handler) { _eventHandlers.push_back(handler); }
private: private:
Updater();
struct RootDirectory
{
Database::MediaDirectory::Type type;
boost::filesystem::path path;
RootDirectory(Database::MediaDirectory::Type t, boost::filesystem::path p) : type(t), path(p) {}
};
// Job handling // Job handling
void processNextJob(); void processNextJob();
void scheduleScan(boost::posix_time::time_duration duration); void scheduleScan(boost::posix_time::time_duration duration);
@@ -103,19 +77,13 @@ class Updater
// Update database (scheduled callback) // Update database (scheduled callback)
void process(boost::system::error_code ec); void process(boost::system::error_code ec);
// Check if a file exists and is still in a root directory void processRootDirectory( boost::filesystem::path rootDirectory, Stats& stats);
static bool checkFile(const boost::filesystem::path& p,
const std::vector<boost::filesystem::path>& rootDirectories,
const std::vector<boost::filesystem::path>& extensions);
void processRootDirectory( RootDirectory rootDirectory, Stats& stats);
// Helpers // Helpers
Database::Artist::pointer getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& MBID); Database::Artist::pointer getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& MBID);
Database::Release::pointer getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& MBID); Database::Release::pointer getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& MBID);
std::vector<Database::Cluster::pointer> getGenreClusters( const std::list<std::string>& names); std::vector<Database::Cluster::pointer> getClusters( const MetaData::Clusters& names);
void updateFileExtensions(); void refreshScanSettings();
// Audio // Audio
void checkAudioFiles( Stats& stats ); void checkAudioFiles( Stats& stats );
@@ -128,47 +96,23 @@ class Updater
bool _running; bool _running;
Wt::WIOService _ioService; Wt::WIOService _ioService;
Wt::Signal<Stats> _sigScanComplete; Wt::Signal<Stats> _sigScanComplete;
Wt::Signal<bool, Artist::id_type> _sigArtistChanged; Wt::Signal<Database::Track::pointer> _sigModifiedTrack;
Wt::Signal<bool, Release::id_type> _sigReleaseChanged; Wt::Signal<Database::Track::pointer> _sigAddedTrack;
SigTrackChanged _sigTrackChanged; Wt::Signal<Database::Track::pointer> _sigRemovedTrack;
std::mutex _mutex;
boost::asio::deadline_timer _scheduleTimer; boost::asio::deadline_timer _scheduleTimer;
Database::Handler* _db = nullptr; Database::Handler _db;
std::vector<boost::filesystem::path> _audioFileExtensions; // Scan settings
std::vector<boost::filesystem::path> _videoFileExtensions; std::vector<boost::filesystem::path> _fileExtensions;
std::vector<boost::filesystem::path> _rootDirectories;
MetaData::TagLibParser _metadataParser; MetaData::TagLibParser _metadataParser;
std::list<std::shared_ptr<UpdaterEventHandler> > _eventHandlers; }; // class MediaScanner
}; // class Updater } // Scanner
// 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();
}
class UpdaterEventHandler
{
public:
// called when all the files have been scanned by the updater
virtual void handleFilesUpdated(void) = 0;
private:
};
} // Database
+13 -6
View File
@@ -56,14 +56,15 @@ Filters::showDialog()
{ {
Wt::Dbo::Transaction transaction(DboSession()); Wt::Dbo::Transaction transaction(DboSession());
auto types = Database::Cluster::getAllTypes(DboSession()); auto types = Database::ClusterType::getAll(DboSession());
for (auto type : types) for (auto type : types)
typeCombo->addItem(Wt::WString::fromUTF8(type)); typeCombo->addItem(Wt::WString::fromUTF8(type->getName()));
if (!types.empty()) if (!types.empty())
{ {
auto values = Database::Cluster::getByType(DboSession(), types.front()); auto values = types.front()->getClusters();
for (auto value : values) for (auto value : values)
{ {
@@ -76,13 +77,15 @@ Filters::showDialog()
typeCombo->changed().connect(std::bind([=] typeCombo->changed().connect(std::bind([=]
{ {
auto type = typeCombo->valueText().toUTF8(); auto name = typeCombo->valueText().toUTF8();
valueCombo->clear(); valueCombo->clear();
Wt::Dbo::Transaction transaction(DboSession()); Wt::Dbo::Transaction transaction(DboSession());
auto values = Database::Cluster::getByType(DboSession(), type); auto clusterType = Database::ClusterType::getByName(DboSession(), name);
auto values = clusterType->getClusters();
for (auto value : values) for (auto value : values)
{ {
if (_filterIds.find(value.id()) == _filterIds.end()) if (_filterIds.find(value.id()) == _filterIds.end())
@@ -108,7 +111,11 @@ Filters::showDialog()
Wt::Dbo::Transaction transaction(DboSession()); Wt::Dbo::Transaction transaction(DboSession());
auto cluster = Database::Cluster::get(DboSession(), type, value); auto clusterType = Database::ClusterType::getByName(DboSession(), type);
if (!clusterType)
return;
auto cluster = clusterType->getCluster(value);
if (!cluster) if (!cluster)
return; return;
+2 -2
View File
@@ -1,7 +1,7 @@
TESTS = sql-query database-user TESTS = sql-query database-user
check_PROGRAMS = sql-query database-user test-wt test-avmetadata check_PROGRAMS = test-avmetadata
database_basics_SOURCES = \ database_basics_SOURCES = \
$(srcdir)/CheckDbBasics.cpp \ $(srcdir)/CheckDbBasics.cpp \
@@ -88,7 +88,7 @@ test_wt_audio_LDADD=$(MAGICKXX_LIBS)
test_avmetadata_SOURCES = TestAvMetadata.cpp \ test_avmetadata_SOURCES = TestAvMetadata.cpp \
$(top_srcdir)/src/logger/Logger.cpp \ $(top_srcdir)/src/utils/Logger.cpp \
$(top_srcdir)/src/utils/Utils.cpp \ $(top_srcdir)/src/utils/Utils.cpp \
$(top_srcdir)/src/metadata/AvFormat.cpp \ $(top_srcdir)/src/metadata/AvFormat.cpp \
$(top_srcdir)/src/metadata/TagLibParser.cpp \ $(top_srcdir)/src/metadata/TagLibParser.cpp \
+17 -7
View File
@@ -26,20 +26,20 @@ int main(int argc, char *argv[])
MetaData::AvFormat avFormatParser; MetaData::AvFormat avFormatParser;
MetaData::TagLibParser tagLibParser; MetaData::TagLibParser tagLibParser;
std::vector<MetaData::Parser*> parsers = { &avFormatParser, &tagLibParser }; std::vector<MetaData::Parser*> parsers = {&avFormatParser, &tagLibParser };
for (auto& parser : parsers) for (auto& parser : parsers)
{ {
MetaData::Items items; boost::optional<MetaData::Items> items = parser->parse(argv[1]);
if (!parser->parse(argv[1], items)) if (!items)
{ {
std::cout << "Parsing failed" << std::endl; std::cout << "Parsing failed" << std::endl;
continue; continue;
} }
std::cout << "Items:" << std::endl; std::cout << "Items:" << std::endl;
for (auto item : items) for (auto item : (*items))
{ {
switch (item.first) switch (item.first)
{ {
@@ -55,9 +55,15 @@ int main(int argc, char *argv[])
std::cout << "Album: " << boost::any_cast<std::string>(item.second) << std::endl; std::cout << "Album: " << boost::any_cast<std::string>(item.second) << std::endl;
break; break;
case MetaData::Type::Genres: case MetaData::Type::Clusters:
for (auto& genre : boost::any_cast<std::list<std::string> >(item.second)) for (const auto& cluster : boost::any_cast<MetaData::Clusters>(item.second))
std::cout << "Genre: " << genre << std::endl; {
std::cout << "Cluster: " << cluster.first << std::endl;
for (const auto name : cluster.second)
{
std::cout << "\t" << name << std::endl;
}
}
break; break;
case MetaData::Type::Duration: case MetaData::Type::Duration:
@@ -113,6 +119,10 @@ int main(int argc, char *argv[])
std::cout << "MusicBrainzRecordingID: " << boost::any_cast<std::string>(item.second) << std::endl; std::cout << "MusicBrainzRecordingID: " << boost::any_cast<std::string>(item.second) << std::endl;
break; break;
case MetaData::Type::AcoustID:
std::cout << "AcoustID: " << boost::any_cast<std::string>(item.second) << std::endl;
break;
default: default:
break; break;
} }