[DB] Switched from ffmpeg to TagLib in order to get tags
This commit is contained in:
@@ -23,7 +23,7 @@ Please note some media files may require significant CPU usage to be transcoded.
|
||||
### Debian or Ubuntu packages
|
||||
|
||||
```sh
|
||||
$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev
|
||||
$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev libcurl-dev libcurlpp-dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
@@ -94,6 +94,21 @@ AC_CHECK_LIB( [boost_thread],
|
||||
,
|
||||
[AC_MSG_ERROR([libboost_thread not found!])])
|
||||
|
||||
AC_CHECK_LIB( [curlpp],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libcurlpp not found!])])
|
||||
|
||||
AC_CHECK_LIB( [curl],
|
||||
[curl_easy_cleanup],
|
||||
,
|
||||
[AC_MSG_ERROR([libcurl not found!])])
|
||||
|
||||
AC_CHECK_LIB( [tag],
|
||||
[main],
|
||||
,
|
||||
[AC_MSG_ERROR([libtag not found!])])
|
||||
|
||||
AC_CONFIG_FILES([Makefile
|
||||
src/Makefile
|
||||
test/Makefile])
|
||||
|
||||
@@ -21,6 +21,7 @@ lms_SOURCES = \
|
||||
$(srcdir)/image/Image.cpp \
|
||||
$(srcdir)/logger/Logger.cpp \
|
||||
$(srcdir)/metadata/AvFormat.cpp \
|
||||
$(srcdir)/metadata/TagLibParser.cpp \
|
||||
$(srcdir)/ui/LmsApplication.cpp \
|
||||
$(srcdir)/ui/auth/LmsAuth.cpp \
|
||||
$(srcdir)/ui/audio/AudioPlayer.cpp \
|
||||
|
||||
@@ -34,41 +34,44 @@ Classifier::Classifier(Wt::Dbo::SqlConnectionPool& connectionPool)
|
||||
{}
|
||||
|
||||
void
|
||||
Classifier::processTrackUpdate(bool added, Track::id_type trackId)
|
||||
Classifier::processTrackUpdate(bool added, Track::id_type trackId, std::string mbid, boost::filesystem::path path)
|
||||
{
|
||||
if (!added)
|
||||
return;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Processing track id " << trackId;
|
||||
|
||||
boost::filesystem::path path;
|
||||
|
||||
if (mbid.empty())
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
|
||||
Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
path = track->getPath();
|
||||
|
||||
// Remove outdated features
|
||||
for (auto feature : track->getFeatures())
|
||||
feature.remove();
|
||||
// TODO compute from file
|
||||
LMS_LOG(CLASSIFICATION, INFO) << "File '" << path << "' has no MBID: skipping feature extraction";
|
||||
return;
|
||||
}
|
||||
|
||||
boost::property_tree::ptree pt;
|
||||
if (!::Feature::Extractor::getLowLevel(pt, path))
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
boost::property_tree::write_json(oss, pt);
|
||||
|
||||
if (::Feature::Extractor::getLowLevel(pt, mbid))
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
std::ostringstream oss;
|
||||
boost::property_tree::write_json(oss, pt);
|
||||
|
||||
Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||
Feature::pointer feature = Feature::create( _db.getSession(), track, "low_level", oss.str());
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
|
||||
Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||
Feature::create( _db.getSession(), track, "low_level", oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
pt.clear();
|
||||
if (::Feature::Extractor::getHighLevel(pt, mbid))
|
||||
{
|
||||
std::ostringstream oss;
|
||||
boost::property_tree::write_json(oss, pt);
|
||||
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
|
||||
Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||
Feature::create( _db.getSession(), track, "high_level", oss.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,11 +819,39 @@ Classifier::processDatabaseUpdate(Updater::Stats stats)
|
||||
trackClusters[coordinates.first][coordinates.second].push_back({trackIds[id], entry, maxValue});
|
||||
}
|
||||
|
||||
// Create clusters
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Erasing old clusters";
|
||||
{
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
Cluster::removeByType(_db.getSession(), "similarity");
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Creating new cluster...";
|
||||
for (std::size_t i = 0; i < nbRows; i++)
|
||||
{
|
||||
for (std::size_t j = 0; j < nbColumns; j++)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Creating cluster " << i << " " << j;
|
||||
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
|
||||
Cluster::pointer cluster = Cluster::create(_db.getSession(), "similarity", "cluster_" + std::to_string(i) + "_" + std::to_string(j));
|
||||
|
||||
for (auto track : trackClusters[i][j])
|
||||
{
|
||||
Track::pointer t = Database::Track::getById(_db.getSession(), track.trackId);
|
||||
cluster.modify()->addTrack(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (std::size_t i = 0; i < nbRows; i++)
|
||||
{
|
||||
for (std::size_t j = 0; j < nbColumns; j++)
|
||||
{
|
||||
|
||||
|
||||
std::cout << "Cluster [" << i << "," << j << "] - ";
|
||||
|
||||
// Display the neuron for this cluster
|
||||
|
||||
@@ -42,7 +42,7 @@ class Classifier
|
||||
public:
|
||||
Classifier(Wt::Dbo::SqlConnectionPool& connectionPool);
|
||||
|
||||
void processTrackUpdate(bool added, Track::id_type trackId);
|
||||
void processTrackUpdate(bool added, Track::id_type trackId, std::string mbid, boost::filesystem::path p);
|
||||
void processDatabaseUpdate(Updater::Stats stats);
|
||||
|
||||
private:
|
||||
|
||||
@@ -103,9 +103,11 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
|
||||
_session.execute("CREATE INDEX artist_name_idx ON artist(name)");
|
||||
_session.execute("CREATE INDEX cluster_name_idx ON cluster(name)");
|
||||
_session.execute("CREATE INDEX cluster_type_idx ON cluster(type)");
|
||||
_session.execute("CREATE INDEX cluster_name_type_idx ON cluster(name, type)");
|
||||
_session.execute("CREATE INDEX release_name_idx ON release(name)");
|
||||
_session.execute("CREATE INDEX track_name_idx ON track(name)");
|
||||
_session.execute("CREATE INDEX feature_type_idx ON feature(type)");
|
||||
_session.execute("CREATE INDEX feature_track_type_idx ON feature(track_id,type)");
|
||||
}
|
||||
catch(std::exception& e) {
|
||||
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
||||
|
||||
@@ -537,6 +537,14 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Updating '" << file << "'";
|
||||
|
||||
// TODO Remove the songs from its clusters
|
||||
// TODO Remove the features of this song
|
||||
track.remove();
|
||||
track.flush();
|
||||
|
||||
track = Track::create(_db->getSession(), file);
|
||||
|
||||
stats.nbModified++;
|
||||
}
|
||||
|
||||
@@ -589,9 +597,9 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
|
||||
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) );
|
||||
}
|
||||
|
||||
if (items.find(MetaData::Type::MusicBrainzTrackID) != items.end())
|
||||
if (items.find(MetaData::Type::MusicBrainzRecordingID) != items.end())
|
||||
{
|
||||
track.modify()->setMBID( boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzTrackID]) );
|
||||
track.modify()->setMBID( boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzRecordingID]) );
|
||||
}
|
||||
|
||||
if (items.find(MetaData::Type::HasCover) != items.end())
|
||||
@@ -603,7 +611,7 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
|
||||
|
||||
transaction.commit();
|
||||
|
||||
_sigTrackChanged.emit(true, track.id());
|
||||
_sigTrackChanged.emit(true, track.id(), track->getMBID(), track->getPath());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WIOService>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <boost/asio/deadline_timer.hpp>
|
||||
|
||||
#include "metadata/AvFormat.hpp"
|
||||
#include <Wt/WIOService>
|
||||
#include <Wt/WSignal>
|
||||
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
|
||||
#include "database/DatabaseHandler.hpp"
|
||||
|
||||
@@ -65,7 +65,11 @@ class Updater
|
||||
|
||||
// Emitted when a track changed
|
||||
// true -> added or modified, false -> to be deleted
|
||||
Wt::Signal<bool, Track::id_type>& trackChanged() { return _sigTrackChanged; }
|
||||
// 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; }
|
||||
|
||||
@@ -117,7 +121,7 @@ class Updater
|
||||
Wt::Signal<Stats> _sigScanComplete;
|
||||
Wt::Signal<bool, Artist::id_type> _sigArtistChanged;
|
||||
Wt::Signal<bool, Release::id_type> _sigReleaseChanged;
|
||||
Wt::Signal<bool, Track::id_type> _sigTrackChanged;
|
||||
SigTrackChanged _sigTrackChanged;
|
||||
std::mutex _mutex;
|
||||
|
||||
boost::asio::deadline_timer _scheduleTimer;
|
||||
@@ -127,7 +131,7 @@ class Updater
|
||||
std::vector<boost::filesystem::path> _audioFileExtensions;
|
||||
std::vector<boost::filesystem::path> _videoFileExtensions;
|
||||
|
||||
MetaData::AvFormat _metadataParser;
|
||||
MetaData::TagLibParser _metadataParser;
|
||||
|
||||
|
||||
}; // class Updater
|
||||
|
||||
@@ -247,6 +247,13 @@ Cluster::create(Wt::Dbo::Session& session, std::string type, std::string name)
|
||||
return session.add(new Cluster(type, name));
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::removeByType(Wt::Dbo::Session& session, std::string type)
|
||||
{
|
||||
session.execute( "DELETE FROM cluster WHERE type = ?").bind(type);
|
||||
}
|
||||
|
||||
|
||||
Wt::Dbo::Query<Cluster::pointer>
|
||||
Cluster::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
|
||||
{
|
||||
@@ -318,6 +325,5 @@ Feature::getByTrack(Wt::Dbo::Session& session, Track::id_type trackId, const std
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class Cluster
|
||||
static pointer getNone(Wt::Dbo::Session& session);
|
||||
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 Wt::Dbo::collection<pointer> getByType(Wt::Dbo::Session& session, std::stirng type);
|
||||
|
||||
// MVC models for the user interface
|
||||
// ClusterID, type, name, track count
|
||||
@@ -65,12 +66,16 @@ class Cluster
|
||||
// Create utility
|
||||
static pointer create(Wt::Dbo::Session& session, std::string type, std::string name);
|
||||
|
||||
// Remove utility
|
||||
static void removeByType(Wt::Dbo::Session& session, std::string type);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName(void) const { return _name; }
|
||||
const std::string& getType(void) const { return _type; }
|
||||
bool isNone(void) const;
|
||||
const Wt::Dbo::collection< Wt::Dbo::ptr<Track> >& getTracks() const { return _tracks;}
|
||||
|
||||
void addTrack(Wt::Dbo::dbo_traits<Track>::IdType trackId);
|
||||
void addTrack(Wt::Dbo::ptr<Track> track) { _tracks.insert(track); }
|
||||
|
||||
template<class Action>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <curlpp/cURLpp.hpp>
|
||||
#include <curlpp/Options.hpp>
|
||||
|
||||
#include <pstreams/pstream.h>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
#include "FeatureExtractor.hpp"
|
||||
|
||||
namespace Feature {
|
||||
|
||||
static boost::filesystem::path extractorPath = boost::filesystem::path();
|
||||
|
||||
bool
|
||||
Extractor::init(void)
|
||||
{
|
||||
static const std::string execName = "streaming_extractor_music";
|
||||
extractorPath = searchExecPath(execName);
|
||||
|
||||
if (extractorPath.empty())
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "Failed to find path to " << execName;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Extractor::Extractor()
|
||||
{ }
|
||||
|
||||
|
||||
|
||||
static bool fetchJSONData(boost::property_tree::ptree& pt, std::string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
curlpp::Cleanup myCleanup;
|
||||
|
||||
std::ostringstream os;
|
||||
os << curlpp::options::Url(url);
|
||||
|
||||
std::istringstream iss(os.str());
|
||||
boost::property_tree::ptree res;
|
||||
boost::property_tree::json_parser::read_json(iss, res);
|
||||
|
||||
pt = res;
|
||||
}
|
||||
catch( curlpp::RuntimeError &e )
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "curlpp error: " << e.what();
|
||||
return false;
|
||||
}
|
||||
catch( curlpp::LogicError &e )
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "curlpp error: " << e.what();
|
||||
return false;
|
||||
}
|
||||
catch ( boost::property_tree::ptree_error& e)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "JSON paring failed: " << e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Extractor::getLowLevel(boost::property_tree::ptree& pt, std::string mbid)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, DEBUG) << "Trying to fetch low level metadata for track '" << mbid << "' on AcousticBrainz";
|
||||
|
||||
boost::property_tree::ptree res;
|
||||
if (!fetchJSONData(res, "https://acousticbrainz.org/" + mbid + "/low-level"))
|
||||
return false;
|
||||
|
||||
auto message = res.get_child_optional("message");
|
||||
if (message)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "Cannot get data on AcousticBrainz: " << message->data();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lowlevel = res.get_child_optional("lowlevel");
|
||||
if (!lowlevel)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "Low level data not found!";
|
||||
return false;
|
||||
}
|
||||
|
||||
res.erase("metadata");
|
||||
|
||||
pt = res;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
Extractor::getHighLevel(boost::property_tree::ptree& pt, std::string mbid)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, DEBUG) << "Trying to fetch high level metadata for track '" << mbid << "' on AcousticBrainz";
|
||||
|
||||
boost::property_tree::ptree res;
|
||||
if (!fetchJSONData(res, "https://acousticbrainz.org/" + mbid + "/high-level"))
|
||||
return false;
|
||||
|
||||
auto message = res.get_child_optional("message");
|
||||
if (message)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "Cannot get data on AcousticBrainz: " << message->data();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lowlevel = res.get_child_optional("highlevel");
|
||||
if (!lowlevel)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "High level data not found!";
|
||||
return false;
|
||||
}
|
||||
|
||||
pt = res;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Extractor::getLowLevel(boost::property_tree::ptree& pt, boost::filesystem::path path)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, DEBUG) << "Extracting low level data from '" << path << "'";
|
||||
|
||||
if (extractorPath.empty())
|
||||
return false;
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
args.push_back(extractorPath.string());
|
||||
args.push_back(path.string());
|
||||
args.push_back("-"); // output to stdout
|
||||
|
||||
std::string jsonData;
|
||||
{
|
||||
redi::ipstream in;
|
||||
in.open(extractorPath.string(), args);
|
||||
if (!in.is_open())
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "Exec failed!";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool firstLineHit = false;
|
||||
|
||||
std::string line;
|
||||
while(std::getline(in, line))
|
||||
{
|
||||
if (!firstLineHit)
|
||||
{
|
||||
if (line != "{")
|
||||
continue;
|
||||
|
||||
firstLineHit = true;
|
||||
}
|
||||
jsonData += line;
|
||||
jsonData += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
std::istringstream iss(jsonData);
|
||||
boost::property_tree::json_parser::read_json(iss, pt);
|
||||
|
||||
pt.erase("metadata");
|
||||
}
|
||||
catch ( boost::property_tree::ptree_error& e)
|
||||
{
|
||||
LMS_LOG(CLASSIFICATION, ERROR) << "JSON paring failed: " << e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Feature
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Feature {
|
||||
|
||||
class Extractor
|
||||
{
|
||||
public:
|
||||
static bool init(void);
|
||||
|
||||
static bool getLowLevel(boost::property_tree::ptree& pt, boost::filesystem::path path);
|
||||
static bool getLowLevel(boost::property_tree::ptree& pt, std::string mbid);
|
||||
|
||||
static bool getHighLevel(boost::property_tree::ptree& pt, std::string mbid);
|
||||
|
||||
private:
|
||||
Extractor();
|
||||
};
|
||||
|
||||
} // namespace Feature
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ int main(int argc, char* argv[])
|
||||
Database::Classifier dbClassifier(*connectionPool);
|
||||
|
||||
// Connect the classifier to the update events
|
||||
dbUpdater.trackChanged().connect(std::bind(&Database::Classifier::processTrackUpdate, &dbClassifier, std::placeholders::_1, std::placeholders::_2));
|
||||
dbUpdater.trackChanged().connect(std::bind(&Database::Classifier::processTrackUpdate, &dbClassifier, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||
dbUpdater.scanComplete().connect(std::bind(&Database::Classifier::processDatabaseUpdate, &dbClassifier, std::placeholders::_1));
|
||||
|
||||
// bind entry point
|
||||
|
||||
@@ -191,6 +191,7 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
|
||||
{
|
||||
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( stringToUTF8(it->second)) ));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace MetaData
|
||||
MusicBrainzArtistID, // string
|
||||
MusicBrainzAlbumID, // string
|
||||
MusicBrainzTrackID, // string
|
||||
MusicBrainzRecordingID, // string
|
||||
};
|
||||
|
||||
// Used by Streams
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
|
||||
#include "logger/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "TagLibParser.hpp"
|
||||
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
bool
|
||||
TagLibParser::parse(const boost::filesystem::path& p, Items& items)
|
||||
{
|
||||
TagLib::FileRef f(p.string().c_str(),
|
||||
true, // read audio properties
|
||||
TagLib::AudioProperties::Average);
|
||||
|
||||
if (f.isNull())
|
||||
return false;
|
||||
|
||||
if (!f.audioProperties())
|
||||
return false;
|
||||
|
||||
{
|
||||
TagLib::AudioProperties *properties = f.audioProperties();
|
||||
|
||||
boost::posix_time::time_duration duration = boost::posix_time::seconds(properties->length());
|
||||
|
||||
items.insert( std::make_pair(MetaData::Type::Duration, duration) );
|
||||
|
||||
MetaData::AudioStream audioStream = { .desc = "", .bitRate = static_cast<std::size_t>(properties->bitrate()) };
|
||||
items.insert( std::make_pair(MetaData::Type::AudioStreams, std::vector<MetaData::AudioStream>(1, audioStream ) ));
|
||||
}
|
||||
|
||||
if (f.tag())
|
||||
{
|
||||
TagLib::PropertyMap tags = f.file()->properties();
|
||||
|
||||
for(TagLib::PropertyMap::ConstIterator itElem = tags.begin(); itElem != tags.end(); ++itElem)
|
||||
{
|
||||
const std::string tag = itElem->first.to8Bit(true);
|
||||
const TagLib::StringList &values = itElem->second;
|
||||
|
||||
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
|
||||
continue;
|
||||
|
||||
// TODO validate MBID format
|
||||
|
||||
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 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))));
|
||||
else if (tag == "TRACKNUMBER")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
auto strings = splitString(values.front().to8Bit(), "/");
|
||||
|
||||
if (!strings.empty())
|
||||
{
|
||||
std::size_t number;
|
||||
if (readAs<std::size_t>(strings[0], number))
|
||||
items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
|
||||
|
||||
if (strings.size() > 1)
|
||||
{
|
||||
std::size_t totalNumber;
|
||||
if (readAs<std::size_t>(strings[1], totalNumber))
|
||||
items.insert( std::make_pair(MetaData::Type::TotalTrack, totalNumber ));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tag == "DISCNUMBER")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
auto strings = splitString(values.front().to8Bit(), "/");
|
||||
|
||||
if (!strings.empty())
|
||||
{
|
||||
std::size_t number;
|
||||
if (readAs<std::size_t>(strings[0], number))
|
||||
items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
|
||||
|
||||
if (strings.size() > 1)
|
||||
{
|
||||
std::size_t totalNumber;
|
||||
if (readAs<std::size_t>(strings[1], totalNumber))
|
||||
items.insert( std::make_pair(MetaData::Type::TotalDisc, totalNumber ));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE")
|
||||
{
|
||||
boost::posix_time::ptime p;
|
||||
if (readAsPosixTime(values.front().to8Bit(), p))
|
||||
items.insert( std::make_pair(MetaData::Type::Date, p));
|
||||
}
|
||||
else if (tag == "ORIGINALDATE")
|
||||
{
|
||||
boost::posix_time::ptime p;
|
||||
if (readAsPosixTime(values.front().to8Bit(), p))
|
||||
{
|
||||
// Take priority on original year
|
||||
items.erase( MetaData::Type::OriginalDate );
|
||||
items.insert( std::make_pair(MetaData::Type::OriginalDate, p));
|
||||
}
|
||||
}
|
||||
else if (tag == "ORIGINALYEAR")
|
||||
{
|
||||
// lower priority than original date
|
||||
if (items.find(MetaData::Type::OriginalDate) == items.end())
|
||||
{
|
||||
boost::posix_time::ptime p;
|
||||
if (readAsPosixTime(values.front().to8Bit(), p))
|
||||
items.insert( std::make_pair(MetaData::Type::OriginalDate, p));
|
||||
}
|
||||
}
|
||||
else if (tag == "GENRE")
|
||||
{
|
||||
std::list<std::string> genreList;
|
||||
for (TagLib::StringList::ConstIterator itGenre = values.begin(); itGenre != values.end(); ++itGenre)
|
||||
genreList.push_back(itGenre->to8Bit(true));
|
||||
|
||||
items.insert( std::make_pair(MetaData::Type::Genres, genreList) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MetaData.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
// Parse that makes use of AvFormat
|
||||
class TagLibParser : public Parser
|
||||
{
|
||||
public:
|
||||
bool parse(const boost::filesystem::path& p, Items& items);
|
||||
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
Reference in New Issue
Block a user