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)
- 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?)
- 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]
- 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?)
- Implement a play queue
- Implement a decent player
- Tags cloud for artist/release
- Dedicate a color for each tag type
[REST 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/Track.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/FeatureStore.cpp \
$(srcdir)/image/Image.cpp \
$(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/metadata/TagLibParser.cpp \
$(srcdir)/scanner/MediaScanner.cpp \
$(srcdir)/ui/ArtistView.cpp \
$(srcdir)/ui/ArtistsView.cpp \
$(srcdir)/ui/Explore.cpp \
+13 -12
View File
@@ -17,6 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <boost/make_unique.hpp>
#include <Wt/Dbo/FixedSqlConnectionPool>
#include <Wt/Dbo/backend/Sqlite3>
@@ -83,22 +85,25 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
_session.mapClass<Database::Artist>("artist");
_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::PlaylistEntry>("playlist_entry");
_session.mapClass<Database::Release>("release");
_session.mapClass<Database::MediaDirectory>("media_directory");
_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::AuthIdentityType>("auth_identity");
_session.mapClass<Database::AuthInfo::AuthTokenType>("auth_token");
_session.mapClass<Database::User>("user");
try {
Wt::Dbo::Transaction transaction(_session);
_session.createTables();
LMS_LOG(DB, INFO) << "Tables created";
}
catch(std::exception& e) {
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
@@ -108,21 +113,17 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
Wt::Dbo::Transaction transaction(_session);
// 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 artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_idx ON track(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_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
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"))
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());
connection->executeSql("pragma journal_mode=WAL");
// connection->executeSql("pragma journal_mode=WAL");
connection->setProperty("show-queries", "true");
return new Wt::Dbo::FixedSqlConnectionPool(connection, 1);
+1
View File
@@ -21,6 +21,7 @@
#define DATABASE_HANDLER_HPP
#include <boost/filesystem.hpp>
#include <memory>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/SqlConnectionPool>
+4 -19
View File
@@ -23,16 +23,15 @@
namespace Database {
MediaDirectory::MediaDirectory(boost::filesystem::path p, Type type)
: _type(type),
_path(stringTrimEnd(p.string(), "/\\"))
MediaDirectory::MediaDirectory(boost::filesystem::path p)
: _path(stringTrimEnd(p.string(), "/\\"))
{
}
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
@@ -50,20 +49,6 @@ MediaDirectory::getAll(Wt::Dbo::Session& session)
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
MediaDirectory::getPath(void) const
{
+3 -13
View File
@@ -31,39 +31,29 @@ namespace Database {
class MediaDirectory
{
public:
typedef Wt::Dbo::ptr<MediaDirectory> pointer;
enum Type {
Audio = 1,
};
MediaDirectory() {}
MediaDirectory(boost::filesystem::path p, Type type);
MediaDirectory(boost::filesystem::path p);
// 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> 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 eraseByPath(Wt::Dbo::Session& session, boost::filesystem::path p);
Type getType(void) const { return _type; }
boost::filesystem::path getPath(void) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _path, "path");
}
private:
Type _type;
std::string _path;
};
} // namespace Database
+59 -38
View File
@@ -224,44 +224,24 @@ Cluster::Cluster()
{
}
Cluster::Cluster(std::string type, std::string name)
:
_type( std::string(type, 0, _maxTypeLength)),
_name( std::string(name, 0, _maxNameLength))
Cluster::Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name)
: _name(std::string(name, 0, _maxNameLength)),
_clusterType(type)
{
}
Wt::Dbo::collection<Cluster::pointer>
Cluster::getAll(Wt::Dbo::Session& session)
{
return session.find<Cluster>();
}
Cluster::pointer
Cluster::get(Wt::Dbo::Session& session, std::string 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)
Cluster::create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name)
{
return session.add(new Cluster(type, name));
}
void
Cluster::remove(Wt::Dbo::Session& session, std::string type)
std::vector<Cluster::pointer>
Cluster::getAll(Wt::Dbo::Session& session)
{
Wt::Dbo::Transaction transaction(session);
session.execute("DELETE FROM cluster WHERE type = ?").bind(type);
Wt::Dbo::collection<Cluster::pointer> res = session.find<Cluster>();
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
Wt::Dbo::Query<Cluster::pointer>
@@ -278,15 +258,6 @@ Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
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>
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());
}
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
+53 -28
View File
@@ -40,64 +40,89 @@ class Artist;
class Release;
class Track;
class PlaylistEntry;
class ClusterType;
class Cluster
class Cluster : public Wt::Dbo::Dbo<Cluster>
{
public:
enum class Type
{
Genre = 1,
Mood = 2,
};
typedef Wt::Dbo::ptr<Cluster> pointer;
typedef Wt::Dbo::dbo_traits<Cluster>::IdType id_type;
Cluster();
Cluster(std::string type, std::string name);
Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name);
// 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 Wt::Dbo::collection<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);
static std::vector<pointer> getAll(Wt::Dbo::Session& session);
// Create utility
static pointer create(Wt::Dbo::Session& session, std::string type, std::string name);
// Remove utility
static void remove(Wt::Dbo::Session& session, std::string type); // nested transaction
static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name);
// Accessors
const std::string& getName(void) const { return _name; }
const std::string& getType(void) const { return _type; }
const Wt::Dbo::collection< Wt::Dbo::ptr<Track> >& getTracks() const { return _tracks;}
Wt::Dbo::ptr<ClusterType> getType() const { return _clusterType; }
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); }
template<class Action>
void persist(Action& a)
{
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);
}
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128;
static const std::size_t _maxTypeLength = 128;
std::string _type;
std::string _name;
Wt::Dbo::ptr<ClusterType> _clusterType;
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
{
public:
@@ -26,11 +26,15 @@
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)
cluster = Cluster::create(UpdaterDboSession(), type, value);
cluster = Cluster::create(UpdaterDboSession(), clusterType, name);
return cluster;
}
@@ -196,7 +200,7 @@ HighLevelCluster::handleFilesUpdated(void)
for (auto cluster : clusters)
{
// Check if removed
if (cluster->getType() != "high_level")
if (cluster->getType()->getName() != "high_level")
continue;
auto it = std::find(newClusterNames.begin(), newClusterNames.end(), cluster->getName());
+9 -10
View File
@@ -29,9 +29,9 @@
#include "image/Image.hpp"
#include "feature/FeatureExtractor.hpp"
#include "database/updater/DatabaseUpdater.hpp"
#include "database/updater/DatabaseFeatureExtractor.hpp"
#include "database/updater/DatabaseHighLevelCluster.hpp"
#include "scanner/MediaScanner.hpp"
//#include "database/updater/DatabaseFeatureExtractor.hpp"
//#include "database/updater/DatabaseHighLevelCluster.hpp"
#include "ui/LmsApplication.hpp"
@@ -125,20 +125,19 @@ int main(int argc, char* argv[])
std::unique_ptr<Wt::Dbo::SqlConnectionPool>
connectionPool( Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"));
Database::Updater& dbUpdater = Database::Updater::instance();
dbUpdater.setConnectionPool(*connectionPool);
Scanner::MediaScanner scanner(*connectionPool);
// Instanciate the updater's event handler. Order is important
dbUpdater.registerEventHandler(std::make_shared<Database::FeatureExtractor>());
dbUpdater.registerEventHandler(std::make_shared<Database::HighLevelCluster>());
// dbUpdater.registerEventHandler(std::make_shared<Database::FeatureExtractor>());
// dbUpdater.registerEventHandler(std::make_shared<Database::HighLevelCluster>());
// bind entry point
server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create,
_1, boost::ref(*connectionPool)));
// Start
LMS_LOG(MAIN, INFO) << "Starting database updater...";
dbUpdater.start();
LMS_LOG(MAIN, INFO) << "Starting Media scanner...";
scanner.start();
LMS_LOG(MAIN, INFO) << "Starting server...";
server.start();
@@ -152,7 +151,7 @@ int main(int argc, char* argv[])
server.stop();
LMS_LOG(MAIN, INFO) << "Stopping database updater...";
dbUpdater.stop();
scanner.stop();
res = EXIT_SUCCESS;
}
+67 -82
View File
@@ -30,19 +30,23 @@
namespace MetaData
{
bool
AvFormat::parse(const boost::filesystem::path& p, Items& items)
AvFormat::AvFormat(const std::map<std::string, std::string>& clusterMap)
: _clusterMap(clusterMap)
{
}
boost::optional<Items>
AvFormat::parse(const boost::filesystem::path& p)
{
Items items;
Av::MediaFile mediaFile(p);
if (!mediaFile.open())
return false;
return boost::none;
if (!mediaFile.scan())
return false;
std::map<std::string, std::string> metadata = mediaFile.getMetaData();
return boost::none;
// Stream info
{
@@ -63,41 +67,6 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
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
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
// 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"))
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "album"))
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "title"))
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "track"))
const std::string tag = boost::to_upper_copy<std::string>(metadata.first);
const std::string value = metadata.second;
#if 0
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
#endif
if (tag == "ARTIST")
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'
auto strings = splitString(it->second, "/");
auto strings = splitString(value, "/");
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'
auto strings = splitString(it->second, "/");
auto strings = splitString(value, "/");
if (strings.size() > 0)
{
@@ -153,49 +130,57 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
}
}
}
else if (boost::iequals(it->first, "date")
|| boost::iequals(it->first, "year")
|| boost::iequals(it->first, "WM/Year"))
else if (tag == "DATE"
|| tag == "YEAR"
|| tag == "WM/Year")
{
boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p))
if (readAsPosixTime(value, p))
items.insert( std::make_pair(MetaData::Type::Date, p));
}
else if (boost::iequals(it->first, "TDOR") // Original release time (ID3v2 2.4)
|| boost::iequals(it->first, "TORY")) // Original release year
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|| tag == "TORY") // Original release year
{
boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p))
if (readAsPosixTime(value, 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
std::list<std::string> genres;
if (readList(it->second, ";,\\", genres))
items.insert( std::make_pair(MetaData::Type::Genres, genres));
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(value)) ));
}
else if (tag == "MUSICBRAINZ ALBUM ID"
|| 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, ";,\\");
}
else if (boost::iequals(it->first, "MusicBrainz Artist Id")
|| boost::iequals(it->first, "MUSICBRAINZ_ARTISTID"))
{
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)) ));
if (!clusterNames.empty())
{
clusters[_clusterMap[tag]] = std::set<std::string>(clusterNames.begin(), clusterNames.end());
}
}
}
return true;
if (!clusters.empty())
items.insert( std::make_pair(MetaData::Type::Clusters, clusters) );
return items;
}
} // 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.
*
@@ -17,8 +17,10 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef METADATA_AVFORMAT_HPP
#define METADATA_AVFORMAT_HPP
#pragma once
#include <map>
#include <string>
#include "MetaData.hpp"
@@ -30,13 +32,19 @@ class AvFormat : public Parser
{
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:
std::map<std::string,std::string> _clusterMap;
};
} // namespace MetaData
#endif
+8 -18
View File
@@ -21,8 +21,10 @@
#define METADATA_HPP
#include <map>
#include <set>
#include <boost/any.hpp>
#include <boost/optional.hpp>
#include <boost/filesystem.hpp>
namespace MetaData
@@ -30,10 +32,11 @@ namespace MetaData
enum class Type
{
// Name Type of the value
Artist, // string
Title, // string
Album, // string
Genres, // list<string>
Clusters, // Clusters, ex: { "genre", {"death metal", "brutal death"} }, { "albumgrouping", {"metal"} }
Duration, // boost::posix_time::time_duration
TrackNumber, // size_t
DiscNumber, // size_t
@@ -43,12 +46,11 @@ namespace MetaData
OriginalDate, // boost::posix_time::ptime
HasCover, // bool
AudioStreams, // vector<AudioStream>
VideoStreams, // vector<VideoStream>
SubtitleStreams, // vector<SubtitleStream>
MusicBrainzArtistID, // string
MusicBrainzAlbumID, // string
MusicBrainzTrackID, // string
MusicBrainzRecordingID, // string
AcoustID, // string
};
// Used by Streams
@@ -58,28 +60,16 @@ namespace MetaData
std::size_t bitRate;
};
struct VideoStream
{
std::string desc;
std::size_t bitRate;
};
struct SubtitleStream
{
std::string desc;
};
// Type and associated data
// 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
{
public:
typedef std::shared_ptr<Parser> pointer;
virtual bool parse(const boost::filesystem::path& p, Items& items) = 0;
virtual boost::optional<Items> parse(const boost::filesystem::path& p) = 0;
};
+45 -19
View File
@@ -32,18 +32,25 @@
namespace MetaData
{
bool
TagLibParser::parse(const boost::filesystem::path& p, Items& items)
TagLibParser::TagLibParser(const std::map<std::string, std::string>& clusterMap)
: _clusterMap(clusterMap)
{
}
boost::optional<Items>
TagLibParser::parse(const boost::filesystem::path& p)
{
TagLib::FileRef f(p.string().c_str(),
true, // read audio properties
TagLib::AudioProperties::Average);
if (f.isNull())
return false;
return boost::none;
if (!f.audioProperties())
return false;
return boost::none;
Items items;
{
TagLib::AudioProperties *properties = f.audioProperties();
@@ -70,32 +77,46 @@ TagLibParser::parse(const boost::filesystem::path& p, Items& items)
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 TagLib::StringList &values = itElem->second;
const std::string tag = property.first.upper().to8Bit(true);
const TagLib::StringList& values = property.second;
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
continue;
// 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")
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( values.front().to8Bit(true))));
else if (tag == "ALBUM")
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( values.front().to8Bit(true))));
else if (tag == "TITLE")
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))));
}
else if (tag == "MUSICBRAINZ_ARTISTID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_ALBUMID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( values.front().to8Bit(true))));
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")
{
// 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));
}
}
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")
{
// Only add once
if (items.find(MetaData::Type::HasCover) == items.end())
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
+14 -2
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,6 +19,9 @@
#pragma once
#include <map>
#include <string>
#include "MetaData.hpp"
namespace MetaData
@@ -28,10 +31,19 @@ namespace MetaData
class TagLibParser : public Parser
{
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:
std::map<std::string,std::string> _clusterMap;
};
} // namespace MetaData
@@ -32,8 +32,7 @@
#include "utils/Path.hpp"
#include "utils/Utils.hpp"
#include "DatabaseUpdater.hpp"
#include "MediaScanner.hpp"
namespace {
@@ -85,20 +84,6 @@ isFileSupported(const boost::filesystem::path& file, const std::vector<boost::fi
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
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 Database {
namespace Scanner {
Updater& Updater::instance(void)
{
static Updater updater;
return updater;
}
using namespace Database;
Updater::Updater()
: _running(true),
_scheduleTimer(_ioService)
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
: _running(false),
_scheduleTimer(_ioService),
_db(connectionPool)
{
_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
Updater::setConnectionPool(Wt::Dbo::SqlConnectionPool& connectionPool)
{
_db = new Database::Handler(connectionPool);
}
void
Updater::restart(void)
MediaScanner::restart(void)
{
stop();
start();
}
void
Updater::start(void)
MediaScanner::start(void)
{
if (_db == nullptr)
throw std::logic_error("uninitialized db!");
_running = true;
// post some jobs in the io_service
@@ -161,7 +139,7 @@ Updater::start(void)
}
void
Updater::stop(void)
MediaScanner::stop(void)
{
_running = false;
@@ -171,9 +149,9 @@ Updater::stop(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!";
scheduleScan( boost::posix_time::seconds(0) );
@@ -181,11 +159,11 @@ Updater::processNextJob(void)
else
{
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;
std::string updatePeriod = Setting::getString(_db->getSession(), "update_period", "never");
std::string updatePeriod = Setting::getString(_db.getSession(), "update_period", "never");
if (updatePeriod == "daily")
{
if (now.time_of_day() < startTime)
@@ -214,72 +192,58 @@ Updater::processNextJob(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;
_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
Updater::scheduleScan( boost::posix_time::ptime time)
MediaScanner::scheduleScan( boost::posix_time::ptime time)
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan 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
Updater::process(boost::system::error_code err)
MediaScanner::process(boost::system::error_code err)
{
if (err)
return;
updateFileExtensions();
refreshScanSettings();
Stats stats;
checkAudioFiles(stats);
std::vector<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)
for (auto rootDirectory : _rootDirectories)
{
if (!_running)
break;
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'...";
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory << "'...";
processRootDirectory(rootDirectory, stats);
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE";
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory << "' DONE";
}
if (_running)
{
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 << ")";
// Update database stats
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
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
if (_running)
{
Setting::setTime(_db->getSession(), "last_scan", now);
Setting::setBool(_db->getSession(), "manual_scan_requested", false);
Setting::setTime(_db.getSession(), "last_scan", now);
Setting::setBool(_db.getSession(), "manual_scan_requested", false);
processNextJob();
@@ -288,26 +252,30 @@ Updater::process(boost::system::error_code err)
}
void
Updater::updateFileExtensions()
MediaScanner::refreshScanSettings()
{
Wt::Dbo::Transaction transaction(_db->getSession());
Wt::Dbo::Transaction transaction(_db.getSession());
_audioFileExtensions.clear();
for (auto extension : splitString(Setting::getString(_db->getSession(), "audio_file_extensions"), " "))
_audioFileExtensions.push_back( extension );
_fileExtensions.clear();
for (auto extension : splitString(Setting::getString(_db.getSession(), "file_extensions"), " "))
_fileExtensions.push_back( extension );
_rootDirectories.clear();
for (auto rootDir : Database::MediaDirectory::getAll(_db.getSession()))
_rootDirectories.push_back(rootDir->getPath());
}
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;
// First try to get by MBID
if (!mbid.empty())
{
artist = Artist::getByMBID( _db->getSession(), mbid );
artist = Artist::getByMBID( _db.getSession(), mbid );
if (!artist)
artist = Artist::create( _db->getSession(), name, mbid);
artist = Artist::create( _db.getSession(), name, mbid);
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)
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())
{
@@ -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
if (!artist)
artist = Artist::create( _db->getSession(), name);
artist = Artist::create( _db.getSession(), name);
return artist;
}
@@ -335,16 +303,16 @@ Updater::getArtist( const boost::filesystem::path& file, const std::string& name
}
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;
// First try to get by MBID
if (!mbid.empty())
{
release = Release::getByMBID( _db->getSession(), mbid );
release = Release::getByMBID( _db.getSession(), mbid );
if (!release)
release = Release::create( _db->getSession(), name, mbid);
release = Release::create( _db.getSession(), name, mbid);
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)
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())
{
@@ -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
if (!release)
release = Release::create( _db->getSession(), name);
release = Release::create( _db.getSession(), name);
return release;
}
@@ -372,32 +340,39 @@ Updater::getRelease( const boost::filesystem::path& file, const std::string& nam
}
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) );
if (!genre)
genre = Cluster::create(_db->getSession(), "Genre", name);
ClusterType::pointer clusterType = ClusterType::getByName(_db.getSession(), clusterNames.first);
if (!clusterType)
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
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 ) ) );
// 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)
{
@@ -406,8 +381,8 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
}
}
MetaData::Items items;
if (!_metadataParser.parse(file, items))
boost::optional<MetaData::Items> items = _metadataParser.parse(file);
if (!items)
{
stats.nbScanErrors++;
return;
@@ -418,15 +393,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::vector<unsigned char> 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 found a least one audio stream
// - the duration is not null
if (items.find(MetaData::Type::AudioStreams) == items.end()
|| boost::any_cast<std::vector<MetaData::AudioStream> >(items[MetaData::Type::AudioStreams]).empty())
if ((*items).find(MetaData::Type::AudioStreams) == (*items).end()
|| boost::any_cast<std::vector<MetaData::AudioStream>> ((*items)[MetaData::Type::AudioStreams]).empty())
{
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++;
return;
}
if (items.find(MetaData::Type::Duration) == items.end()
|| boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]).total_seconds() <= 0)
if ((*items).find(MetaData::Type::Duration) == (*items).end()
|| 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)";
@@ -456,9 +431,9 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
// ***** 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
{
@@ -470,13 +445,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
// ***** Clusters
std::vector< Cluster::pointer > genres;
{
std::list<std::string> genreList;
MetaData::Clusters clusterNames;
if (items.find(MetaData::Type::Genres) != items.end())
genreList = boost::any_cast< std::list<std::string> > (items[MetaData::Type::Genres]);
if ((*items).find(MetaData::Type::Clusters) != (*items).end())
{
clusterNames = boost::any_cast<MetaData::Clusters> ((*items)[MetaData::Type::Clusters]);
}
// TODO rename
genres = getGenreClusters( genreList );
genres = getClusters( clusterNames );
}
// ***** Artist
@@ -485,11 +462,11 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::string artistName;
std::string artistMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzArtistID) != items.end())
artistMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzArtistID] );
if ((*items).find(MetaData::Type::MusicBrainzArtistID) != (*items).end())
artistMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzArtistID] );
if (items.find(MetaData::Type::Artist) != items.end())
artistName = boost::any_cast<std::string>(items[MetaData::Type::Artist]);
if ((*items).find(MetaData::Type::Artist) != (*items).end())
artistName = boost::any_cast<std::string>((*items)[MetaData::Type::Artist]);
artist = getArtist(file, artistName, artistMusicBrainzID);
}
@@ -500,11 +477,11 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
std::string releaseName;
std::string releaseMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzAlbumID) != items.end())
releaseMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzAlbumID] );
if ((*items).find(MetaData::Type::MusicBrainzAlbumID) != (*items).end())
releaseMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzAlbumID] );
if (items.find(MetaData::Type::Album) != items.end())
releaseName = boost::any_cast<std::string>(items[MetaData::Type::Album]);
if ((*items).find(MetaData::Type::Album) != (*items).end())
releaseName = boost::any_cast<std::string>((*items)[MetaData::Type::Album]);
release = getRelease(file, releaseName, releaseMusicBrainzID);
}
@@ -515,7 +492,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
if (!track)
{
// Create a new song
track = Track::create(_db->getSession(), file);
track = Track::create(_db.getSession(), file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'";
stats.nbAdded++;
}
@@ -537,7 +514,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
track.modify()->setRelease(release);
track.modify()->setLastWriteTime(lastWriteTime);
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() );
{
@@ -555,53 +532,54 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
track.modify()->setGenres( trackClusterList );
}
if (items.find(MetaData::Type::TrackNumber) != items.end())
track.modify()->setTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TrackNumber]) );
if ((*items).find(MetaData::Type::TrackNumber) != (*items).end())
track.modify()->setTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TrackNumber]) );
if (items.find(MetaData::Type::TotalTrack) != items.end())
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalTrack]) );
if ((*items).find(MetaData::Type::TotalTrack) != (*items).end())
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalTrack]) );
if (items.find(MetaData::Type::DiscNumber) != items.end())
track.modify()->setDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::DiscNumber]) );
if ((*items).find(MetaData::Type::DiscNumber) != (*items).end())
track.modify()->setDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::DiscNumber]) );
if (items.find(MetaData::Type::TotalDisc) != items.end())
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalDisc]) );
if ((*items).find(MetaData::Type::TotalDisc) != (*items).end())
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalDisc]) );
if (items.find(MetaData::Type::Date) != items.end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::Date]) );
if ((*items).find(MetaData::Type::Date) != (*items).end())
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 (items.find(MetaData::Type::Date) == items.end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) );
if ((*items).find(MetaData::Type::Date) == (*items).end())
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 );
}
transaction.commit();
// TODO check added/modified
_sigAddedTrack.emit(track);
_sigTrackChanged.emit(true, track.id(), track->getMBID(), track->getPath());
transaction.commit();
}
void
Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats)
MediaScanner::processRootDirectory(boost::filesystem::path rootDirectory, Stats& stats)
{
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;
while (!ec && itPath != itEnd)
@@ -614,20 +592,15 @@ Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats)
if (boost::filesystem::is_regular(path))
{
switch( rootDirectory.type )
{
case Database::MediaDirectory::Audio:
if (isFileSupported(path, _audioFileExtensions))
processAudioFile(path, stats );
break;
}
if (isFileSupported(path, _fileExtensions))
processAudioFile(path, stats );
}
}
}
bool
Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions)
// Check if a file exists and is still in a root directory
static bool
checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions)
{
try
{
@@ -678,12 +651,11 @@ Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::fi
}
void
Updater::checkAudioFiles( Stats& stats )
MediaScanner::checkAudioFiles( Stats& stats )
{
LMS_LOG(DBUPDATER, INFO) << "Checking audio files...";
std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db->getSession());;
std::vector<boost::filesystem::path> rootDirs = getRootDirectoriesByType(_db->getSession(), Database::MediaDirectory::Audio);
std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db.getSession());;
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (auto& trackPath : trackPaths)
@@ -691,11 +663,11 @@ Updater::checkAudioFiles( Stats& stats )
if (!_running)
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)
{
track.remove();
@@ -706,25 +678,25 @@ Updater::checkAudioFiles( Stats& stats )
LMS_LOG(DBUPDATER, DEBUG) << "Checking Clusters...";
{
Wt::Dbo::Transaction transaction(_db->getSession());
Wt::Dbo::Transaction transaction(_db.getSession());
// Now process orphan Cluster (no track)
auto genres = Cluster::getAll(_db->getSession());
for (auto genre : genres)
auto clusters = Cluster::getAll(_db.getSession());
for (auto cluster : clusters)
{
if (genre->getTracks().size() == 0)
if (cluster->getTracks().size() == 0)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan genre '" << genre->getName() << "'";
genre.remove();
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
cluster.remove();
}
}
}
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)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
@@ -734,9 +706,9 @@ Updater::checkAudioFiles( Stats& stats )
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)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
@@ -748,19 +720,19 @@ Updater::checkAudioFiles( Stats& stats )
}
void
Updater::checkDuplicatedAudioFiles(Stats& stats)
MediaScanner::checkDuplicatedAudioFiles(Stats& stats)
{
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)
{
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)
{
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
#include <mutex>
#include <list>
#include <boost/asio/deadline_timer.hpp>
#include <Wt/WIOService>
@@ -31,21 +28,14 @@
#include "database/DatabaseHandler.hpp"
namespace Database {
namespace Scanner {
class UpdaterEventHandler;
class Updater
class MediaScanner
{
public:
static Updater& instance();
void setConnectionPool(Wt::Dbo::SqlConnectionPool& connectionPool);
void setAudioExtensions(const std::vector<std::string>& extensions);
void setVideoExtensions(const std::vector<std::string>& extensions);
MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
void start();
void stop();
@@ -64,37 +54,21 @@ class Updater
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; }
// 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:
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
void processNextJob();
void scheduleScan(boost::posix_time::time_duration duration);
@@ -103,19 +77,13 @@ class Updater
// Update database (scheduled callback)
void process(boost::system::error_code ec);
// Check if a file exists and is still in a root directory
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);
void processRootDirectory( boost::filesystem::path rootDirectory, Stats& stats);
// Helpers
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);
std::vector<Database::Cluster::pointer> getGenreClusters( const std::list<std::string>& names);
void updateFileExtensions();
std::vector<Database::Cluster::pointer> getClusters( const MetaData::Clusters& names);
void refreshScanSettings();
// Audio
void checkAudioFiles( Stats& stats );
@@ -128,47 +96,23 @@ class Updater
bool _running;
Wt::WIOService _ioService;
Wt::Signal<Stats> _sigScanComplete;
Wt::Signal<bool, Artist::id_type> _sigArtistChanged;
Wt::Signal<bool, Release::id_type> _sigReleaseChanged;
SigTrackChanged _sigTrackChanged;
std::mutex _mutex;
Wt::Signal<Database::Track::pointer> _sigModifiedTrack;
Wt::Signal<Database::Track::pointer> _sigAddedTrack;
Wt::Signal<Database::Track::pointer> _sigRemovedTrack;
boost::asio::deadline_timer _scheduleTimer;
Database::Handler* _db = nullptr;
Database::Handler _db;
std::vector<boost::filesystem::path> _audioFileExtensions;
std::vector<boost::filesystem::path> _videoFileExtensions;
// Scan settings
std::vector<boost::filesystem::path> _fileExtensions;
std::vector<boost::filesystem::path> _rootDirectories;
MetaData::TagLibParser _metadataParser;
std::list<std::shared_ptr<UpdaterEventHandler> > _eventHandlers;
}; // class MediaScanner
}; // class Updater
// Helper to get the updater session data
static inline Wt::Dbo::Session& UpdaterDboSession()
{
return Updater::instance().getDb().getSession();
}
static inline bool UpdaterQuitRequested()
{
return Updater::instance().quitRequested();
}
class UpdaterEventHandler
{
public:
// called when all the files have been scanned by the updater
virtual void handleFilesUpdated(void) = 0;
private:
};
} // Database
} // Scanner
+13 -6
View File
@@ -56,14 +56,15 @@ Filters::showDialog()
{
Wt::Dbo::Transaction transaction(DboSession());
auto types = Database::Cluster::getAllTypes(DboSession());
auto types = Database::ClusterType::getAll(DboSession());
for (auto type : types)
typeCombo->addItem(Wt::WString::fromUTF8(type));
typeCombo->addItem(Wt::WString::fromUTF8(type->getName()));
if (!types.empty())
{
auto values = Database::Cluster::getByType(DboSession(), types.front());
auto values = types.front()->getClusters();
for (auto value : values)
{
@@ -76,13 +77,15 @@ Filters::showDialog()
typeCombo->changed().connect(std::bind([=]
{
auto type = typeCombo->valueText().toUTF8();
auto name = typeCombo->valueText().toUTF8();
valueCombo->clear();
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)
{
if (_filterIds.find(value.id()) == _filterIds.end())
@@ -108,7 +111,11 @@ Filters::showDialog()
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)
return;
+2 -2
View File
@@ -1,7 +1,7 @@
TESTS = sql-query database-user
check_PROGRAMS = sql-query database-user test-wt test-avmetadata
check_PROGRAMS = test-avmetadata
database_basics_SOURCES = \
$(srcdir)/CheckDbBasics.cpp \
@@ -88,7 +88,7 @@ test_wt_audio_LDADD=$(MAGICKXX_LIBS)
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/metadata/AvFormat.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::TagLibParser tagLibParser;
std::vector<MetaData::Parser*> parsers = { &avFormatParser, &tagLibParser };
std::vector<MetaData::Parser*> parsers = {&avFormatParser, &tagLibParser };
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;
continue;
}
std::cout << "Items:" << std::endl;
for (auto item : items)
for (auto item : (*items))
{
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;
break;
case MetaData::Type::Genres:
for (auto& genre : boost::any_cast<std::list<std::string> >(item.second))
std::cout << "Genre: " << genre << std::endl;
case MetaData::Type::Clusters:
for (const auto& cluster : boost::any_cast<MetaData::Clusters>(item.second))
{
std::cout << "Cluster: " << cluster.first << std::endl;
for (const auto name : cluster.second)
{
std::cout << "\t" << name << std::endl;
}
}
break;
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;
break;
case MetaData::Type::AcoustID:
std::cout << "AcoustID: " << boost::any_cast<std::string>(item.second) << std::endl;
break;
default:
break;
}