From 6f973096f3fba82d3ee0b87e3e9097ce5f4b04f6 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 May 2016 19:58:27 +0200 Subject: [PATCH] [DB] Switched from ffmpeg to TagLib in order to get tags --- README.md | 2 +- configure.ac | 15 ++ src/Makefile.am | 1 + src/database/DatabaseClassifier.cpp | 81 +++++++---- src/database/DatabaseClassifier.hpp | 2 +- src/database/DatabaseHandler.cpp | 2 + src/database/DatabaseUpdater.cpp | 14 +- src/database/DatabaseUpdater.hpp | 18 ++- src/database/Track.cpp | 8 +- src/database/Track.hpp | 5 + src/feature/FeatureExtractor.cpp | 209 ++++++++++++++++++++++++++++ src/feature/FeatureExtractor.hpp | 43 ++++++ src/main/main.cpp | 2 +- src/metadata/AvFormat.cpp | 1 + src/metadata/MetaData.hpp | 1 + src/metadata/TagLibParser.cpp | 164 ++++++++++++++++++++++ src/metadata/TagLibParser.hpp | 38 +++++ 17 files changed, 567 insertions(+), 39 deletions(-) create mode 100644 src/feature/FeatureExtractor.cpp create mode 100644 src/feature/FeatureExtractor.hpp create mode 100644 src/metadata/TagLibParser.cpp create mode 100644 src/metadata/TagLibParser.hpp diff --git a/README.md b/README.md index 82b2b105..9572e688 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/configure.ac b/configure.ac index a5e1e05d..3cd2b78a 100644 --- a/configure.ac +++ b/configure.ac @@ -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]) diff --git a/src/Makefile.am b/src/Makefile.am index ca24f715..ba001bef 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/database/DatabaseClassifier.cpp b/src/database/DatabaseClassifier.cpp index e87ad855..45b8cb98 100644 --- a/src/database/DatabaseClassifier.cpp +++ b/src/database/DatabaseClassifier.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 diff --git a/src/database/DatabaseClassifier.hpp b/src/database/DatabaseClassifier.hpp index a566d291..c8e382b8 100644 --- a/src/database/DatabaseClassifier.hpp +++ b/src/database/DatabaseClassifier.hpp @@ -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: diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index 7e76dfaa..9cf10f7f 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -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(); diff --git a/src/database/DatabaseUpdater.cpp b/src/database/DatabaseUpdater.cpp index 85a2c923..f202284d 100644 --- a/src/database/DatabaseUpdater.cpp +++ b/src/database/DatabaseUpdater.cpp @@ -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(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(items[MetaData::Type::MusicBrainzTrackID]) ); + track.modify()->setMBID( boost::any_cast(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()); } diff --git a/src/database/DatabaseUpdater.hpp b/src/database/DatabaseUpdater.hpp index 3e543951..08ae0498 100644 --- a/src/database/DatabaseUpdater.hpp +++ b/src/database/DatabaseUpdater.hpp @@ -19,14 +19,14 @@ #pragma once -#include -#include - #include #include -#include "metadata/AvFormat.hpp" +#include +#include + +#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& trackChanged() { return _sigTrackChanged; } + // id of the track + // musicbrainz recordid + // path of the track + typedef Wt::Signal SigTrackChanged; + SigTrackChanged& trackChanged() { return _sigTrackChanged; } std::mutex& getMutex(void) { return _mutex; } @@ -117,7 +121,7 @@ class Updater Wt::Signal _sigScanComplete; Wt::Signal _sigArtistChanged; Wt::Signal _sigReleaseChanged; - Wt::Signal _sigTrackChanged; + SigTrackChanged _sigTrackChanged; std::mutex _mutex; boost::asio::deadline_timer _scheduleTimer; @@ -127,7 +131,7 @@ class Updater std::vector _audioFileExtensions; std::vector _videoFileExtensions; - MetaData::AvFormat _metadataParser; + MetaData::TagLibParser _metadataParser; }; // class Updater diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 138e1a52..a46a570b 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -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::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(res.begin(), res.end()); } - } // namespace Database diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 9d412f94..2399e18f 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -55,6 +55,7 @@ class Cluster static pointer getNone(Wt::Dbo::Session& session); static std::vector getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1); static Wt::Dbo::collection getAll(Wt::Dbo::Session& session); + static Wt::Dbo::collection 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 >& getTracks() const { return _tracks;} + void addTrack(Wt::Dbo::dbo_traits::IdType trackId); void addTrack(Wt::Dbo::ptr track) { _tracks.insert(track); } template diff --git a/src/feature/FeatureExtractor.cpp b/src/feature/FeatureExtractor.cpp new file mode 100644 index 00000000..23f54ff5 --- /dev/null +++ b/src/feature/FeatureExtractor.cpp @@ -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 . + */ + +#include +#include + +#include + +#include + +#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 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 + diff --git a/src/feature/FeatureExtractor.hpp b/src/feature/FeatureExtractor.hpp new file mode 100644 index 00000000..ed7169c2 --- /dev/null +++ b/src/feature/FeatureExtractor.hpp @@ -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 . + */ + +#include +#include + +#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 + diff --git a/src/main/main.cpp b/src/main/main.cpp index 806ba1c7..05f6bb3c 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -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 diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp index 51679271..81a95b26 100644 --- a/src/metadata/AvFormat.cpp +++ b/src/metadata/AvFormat.cpp @@ -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; diff --git a/src/metadata/MetaData.hpp b/src/metadata/MetaData.hpp index 9a1728fe..019184a3 100644 --- a/src/metadata/MetaData.hpp +++ b/src/metadata/MetaData.hpp @@ -48,6 +48,7 @@ namespace MetaData MusicBrainzArtistID, // string MusicBrainzAlbumID, // string MusicBrainzTrackID, // string + MusicBrainzRecordingID, // string }; // Used by Streams diff --git a/src/metadata/TagLibParser.cpp b/src/metadata/TagLibParser.cpp new file mode 100644 index 00000000..ce4a7a5b --- /dev/null +++ b/src/metadata/TagLibParser.cpp @@ -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 . + */ + +#include +#include +#include + +#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(properties->bitrate()) }; + items.insert( std::make_pair(MetaData::Type::AudioStreams, std::vector(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(strings[0], number)) + items.insert( std::make_pair(MetaData::Type::TrackNumber, number )); + + if (strings.size() > 1) + { + std::size_t totalNumber; + if (readAs(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(strings[0], number)) + items.insert( std::make_pair(MetaData::Type::DiscNumber, number )); + + if (strings.size() > 1) + { + std::size_t totalNumber; + if (readAs(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 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 + diff --git a/src/metadata/TagLibParser.hpp b/src/metadata/TagLibParser.hpp new file mode 100644 index 00000000..a05364aa --- /dev/null +++ b/src/metadata/TagLibParser.hpp @@ -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 . + */ + +#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 +