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
+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());
-774
View File
@@ -1,774 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdexcept>
#include <boost/filesystem.hpp>
#include <boost/thread.hpp>
#include <boost/asio/placeholders.hpp>
#include "cover/CoverArtGrabber.hpp"
#include "database/Setting.hpp"
#include "database/Types.hpp"
#include "utils/Logger.hpp"
#include "utils/Path.hpp"
#include "utils/Utils.hpp"
#include "DatabaseUpdater.hpp"
namespace {
boost::gregorian::date
getNextDay(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
return *(++it);
}
boost::gregorian::date
getNextMonday(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
++it;
// While it's not monday
while( it->day_of_week() != 1 )
++it;
return *(it);
}
boost::gregorian::date
getNextFirstOfMonth(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
++it;
// While it's not the 1st of the month
while( it->day() != 1 )
++it;
return (*it);
}
bool
isFileSupported(const boost::filesystem::path& file, const std::vector<boost::filesystem::path> extensions)
{
boost::filesystem::path fileExtension = file.extension();
for (auto& extension : extensions)
{
if (extension == fileExtension)
return true;
}
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)
{
boost::filesystem::path curPath = path;
while (curPath.has_parent_path())
{
curPath = curPath.parent_path();
if (curPath == parentPath)
return true;
}
return false;
}
} // namespace
namespace Database {
Updater& Updater::instance(void)
{
static Updater updater;
return updater;
}
Updater::Updater()
: _running(true),
_scheduleTimer(_ioService)
{
_ioService.setThreadCount(1);
}
void
Updater::setConnectionPool(Wt::Dbo::SqlConnectionPool& connectionPool)
{
_db = new Database::Handler(connectionPool);
}
void
Updater::restart(void)
{
stop();
start();
}
void
Updater::start(void)
{
if (_db == nullptr)
throw std::logic_error("uninitialized db!");
_running = true;
// post some jobs in the io_service
processNextJob();
_ioService.start();
}
void
Updater::stop(void)
{
_running = false;
_scheduleTimer.cancel();
_ioService.stop();
}
void
Updater::processNextJob(void)
{
if (Setting::getBool(_db->getSession(), "manual_scan_requested", false))
{
LMS_LOG(DBUPDATER, INFO) << "Manual scan requested!";
scheduleScan( boost::posix_time::seconds(0) );
}
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::gregorian::date nextScanDate;
std::string updatePeriod = Setting::getString(_db->getSession(), "update_period", "never");
if (updatePeriod == "daily")
{
if (now.time_of_day() < startTime)
nextScanDate = now.date();
else
nextScanDate = getNextDay(now.date());
}
else if (updatePeriod == "weekly")
{
if (now.time_of_day() < startTime && now.date().day_of_week() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextMonday(now.date());
}
else if (updatePeriod == "monthly")
{
if (now.time_of_day() < startTime && now.date().day() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextFirstOfMonth(now.date());
}
if (!nextScanDate.is_special())
scheduleScan( boost::posix_time::ptime (nextScanDate, startTime) );
}
}
void
Updater::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) );
}
void
Updater::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) );
}
void
Updater::process(boost::system::error_code err)
{
if (err)
return;
updateFileExtensions();
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)
{
if (!_running)
break;
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'...";
processRootDirectory(rootDirectory, stats);
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE";
}
if (_running)
{
checkDuplicatedAudioFiles(stats);
LMS_LOG(DBUPDATER, INFO) << "Processed all files, now calling listeners...";
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);
// 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);
processNextJob();
scanComplete().emit(stats);
}
}
void
Updater::updateFileExtensions()
{
Wt::Dbo::Transaction transaction(_db->getSession());
_audioFileExtensions.clear();
for (auto extension : splitString(Setting::getString(_db->getSession(), "audio_file_extensions"), " "))
_audioFileExtensions.push_back( extension );
}
Artist::pointer
Updater::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 );
if (!artist)
artist = Artist::create( _db->getSession(), name, mbid);
return artist;
}
// Fall back on artist name (collisions may occur)
if (!name.empty())
{
for (Artist::pointer sameNamedArtist : Artist::getByName( _db->getSession(), name ))
{
if (sameNamedArtist->getMBID().empty())
{
artist = sameNamedArtist;
break;
}
}
// No Artist found with the same name and without MBID -> creating
if (!artist)
artist = Artist::create( _db->getSession(), name);
return artist;
}
return Artist::pointer();
}
Release::pointer
Updater::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 );
if (!release)
release = Release::create( _db->getSession(), name, mbid);
return release;
}
// Fall back on release name (collisions may occur)
if (!name.empty())
{
for (Release::pointer sameNamedRelease : Release::getByName( _db->getSession(), name ))
{
if (sameNamedRelease->getMBID().empty())
{
release = sameNamedRelease;
break;
}
}
// No release found with the same name and without MBID -> creating
if (!release)
release = Release::create( _db->getSession(), name);
return release;
}
return Release::pointer();
}
std::vector<Cluster::pointer>
Updater::getGenreClusters( const std::list<std::string>& names)
{
std::vector< Cluster::pointer > genres;
for (const std::string& name : names)
{
Cluster::pointer genre ( Cluster::get(_db->getSession(), "Genre", name) );
if (!genre)
genre = Cluster::create(_db->getSession(), "Genre", name);
genres.push_back( genre );
}
return genres;
}
void
Updater::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::ptr<Track> track = Track::getByPath(_db->getSession(), file);
if (track && track->getLastWriteTime() == lastWriteTime)
{
stats.nbNoChange++;
return;
}
}
MetaData::Items items;
if (!_metadataParser.parse(file, items))
{
stats.nbScanErrors++;
return;
}
stats.nbScanned++;
std::vector<unsigned char> checksum ;
computeCrc(file, checksum);
Wt::Dbo::Transaction transaction(_db->getSession());
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())
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no audio stream found)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.nbRemoved++;
}
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)
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no duration or duration <= 0)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.nbRemoved++;
}
stats.nbNotImported++;
return;
}
// ***** Title
std::string title;
if (items.find(MetaData::Type::Title) != items.end())
{
title = boost::any_cast<std::string>(items[MetaData::Type::Title]);
}
else
{
// TODO parse file name guess track etc.
// For now juste use file name as title
title = file.filename().string();
}
// ***** Clusters
std::vector< Cluster::pointer > genres;
{
std::list<std::string> genreList;
if (items.find(MetaData::Type::Genres) != items.end())
genreList = boost::any_cast< std::list<std::string> > (items[MetaData::Type::Genres]);
// TODO rename
genres = getGenreClusters( genreList );
}
// ***** Artist
Artist::pointer artist;
{
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::Artist) != items.end())
artistName = boost::any_cast<std::string>(items[MetaData::Type::Artist]);
artist = getArtist(file, artistName, artistMusicBrainzID);
}
// ***** Release
Release::pointer release;
{
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::Album) != items.end())
releaseName = boost::any_cast<std::string>(items[MetaData::Type::Album]);
release = getRelease(file, releaseName, releaseMusicBrainzID);
}
assert(release);
// If file already exist, update data
// Otherwise, create it
if (!track)
{
// Create a new song
track = Track::create(_db->getSession(), file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'";
stats.nbAdded++;
}
else
{
LMS_LOG(DBUPDATER, INFO) << "Updating '" << file << "'";
// Remove the songs from its clusters
for (auto cluster : track->getClusters())
cluster.remove();
stats.nbUpdated++;
}
assert(track);
track.modify()->setChecksum(checksum);
track.modify()->setArtist(artist);
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()->setAddedTime( boost::posix_time::second_clock::local_time() );
{
std::string trackClusterList;
// Product genre list
for (Cluster::pointer genre : genres)
{
if (!trackClusterList.empty())
trackClusterList += ", ";
trackClusterList += genre->getName();
genre.modify()->addTrack(track);
}
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::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::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::OriginalDate) != items.end())
{
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::MusicBrainzRecordingID) != items.end())
{
track.modify()->setMBID( boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzRecordingID]) );
}
if (items.find(MetaData::Type::HasCover) != items.end())
{
bool hasCover = boost::any_cast<bool>(items[MetaData::Type::HasCover]);
track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None );
}
transaction.commit();
_sigTrackChanged.emit(true, track.id(), track->getMBID(), track->getPath());
}
void
Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats)
{
boost::system::error_code ec;
boost::filesystem::recursive_directory_iterator itPath(rootDirectory.path, ec);
boost::filesystem::recursive_directory_iterator itEnd;
while (!ec && itPath != itEnd)
{
boost::filesystem::path path = *itPath;
itPath.increment(ec);
if (!_running)
return;
if (boost::filesystem::is_regular(path))
{
switch( rootDirectory.type )
{
case Database::MediaDirectory::Audio:
if (isFileSupported(path, _audioFileExtensions))
processAudioFile(path, stats );
break;
}
}
}
}
bool
Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions)
{
try
{
bool status = true;
// For each track, make sure the the file still exists
// and still belongs to a root directory
if (!boost::filesystem::exists( p )
|| !boost::filesystem::is_regular( p ) )
{
LMS_LOG(DBUPDATER, INFO) << "Missing file '" << p << "'";
status = false;
}
else
{
bool foundRoot = false;
for (auto& rootDir : rootDirs)
{
if (isPathInParentPath(p, rootDir))
{
foundRoot = true;
break;
}
}
if (!foundRoot)
{
LMS_LOG(DBUPDATER, INFO) << "Out of root file '" << p << "'";
status = false;
}
else if (!isFileSupported(p, extensions))
{
LMS_LOG(DBUPDATER, INFO) << "File format no longer supported for '" << p << "'";
status = false;
}
}
return status;
}
catch (boost::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p << "': " << e.what();
return false;
}
}
void
Updater::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);
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (auto& trackPath : trackPaths)
{
if (!_running)
return;
if (!checkFile(trackPath, rootDirs, _audioFileExtensions))
{
Wt::Dbo::Transaction transaction(_db->getSession());
Track::pointer track = Track::getByPath(_db->getSession(), trackPath);
if (track)
{
track.remove();
stats.nbRemoved++;
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking Clusters...";
{
Wt::Dbo::Transaction transaction(_db->getSession());
// Now process orphan Cluster (no track)
auto genres = Cluster::getAll(_db->getSession());
for (auto genre : genres)
{
if (genre->getTracks().size() == 0)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan genre '" << genre->getName() << "'";
genre.remove();
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking artists...";
{
Wt::Dbo::Transaction transaction(_db->getSession());
auto artists = Artist::getAllOrphans(_db->getSession());
for (auto artist : artists)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
artist.remove();
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking releases...";
{
Wt::Dbo::Transaction transaction(_db->getSession());
auto releases = Release::getAllOrphans(_db->getSession());
for (auto release : releases)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
release.remove();
}
}
LMS_LOG(DBUPDATER, INFO) << "Check audio files done!";
}
void
Updater::checkDuplicatedAudioFiles(Stats& stats)
{
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files";
Wt::Dbo::Transaction transaction(_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());
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) << "Checking duplicated audio files done!";
}
} // namespace Database
-174
View File
@@ -1,174 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <mutex>
#include <list>
#include <boost/asio/deadline_timer.hpp>
#include <Wt/WIOService>
#include <Wt/WSignal>
#include "metadata/TagLibParser.hpp"
#include "database/DatabaseHandler.hpp"
namespace Database {
class UpdaterEventHandler;
class Updater
{
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);
void start();
void stop();
void restart();
struct Stats
{
std::size_t nbNoChange = 0; // no change since last scan
std::size_t nbScanned = 0; // total scanned filed
std::size_t nbScanErrors = 0; // cannot scan file
std::size_t nbNotImported = 0; // Scanned, but not imported (criteria not filled)
std::size_t nbAdded = 0; // Added in DB
std::size_t nbRemoved = 0; // removed from DB
std::size_t nbUpdated = 0; // updated file in DB
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)
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);
void scheduleScan(boost::posix_time::ptime time);
// 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);
// 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();
// Audio
void checkAudioFiles( Stats& stats );
void checkDuplicatedAudioFiles( Stats& stats );
void processAudioFile( const boost::filesystem::path& file, Stats& stats);
// Video
void checkVideoFiles( Stats& stats );
void processVideoFile( const boost::filesystem::path& file, Stats& stats);
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;
boost::asio::deadline_timer _scheduleTimer;
Database::Handler* _db = nullptr;
std::vector<boost::filesystem::path> _audioFileExtensions;
std::vector<boost::filesystem::path> _videoFileExtensions;
MetaData::TagLibParser _metadataParser;
std::list<std::shared_ptr<UpdaterEventHandler> > _eventHandlers;
}; // 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