[DB] Switched from ffmpeg to TagLib in order to get tags

This commit is contained in:
emeric
2016-05-02 19:58:27 +02:00
parent 2975d87889
commit 6f973096f3
17 changed files with 567 additions and 39 deletions
+1 -1
View File
@@ -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
+15
View File
@@ -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])
+1
View File
@@ -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 \
+56 -25
View File
@@ -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
+1 -1
View File
@@ -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:
+2
View File
@@ -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();
+11 -3
View File
@@ -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());
}
+11 -7
View File
@@ -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
+7 -1
View File
@@ -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
+5
View File
@@ -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>
+209
View File
@@ -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
+43
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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;
+1
View File
@@ -48,6 +48,7 @@ namespace MetaData
MusicBrainzArtistID, // string
MusicBrainzAlbumID, // string
MusicBrainzTrackID, // string
MusicBrainzRecordingID, // string
};
// Used by Streams
+164
View File
@@ -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
+38
View File
@@ -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