Files
lms/src/metadata/TagLibParser.cpp
T
2019-07-03 13:32:24 +02:00

313 lines
8.4 KiB
C++

/*
* 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 "TagLibParser.hpp"
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/mpegfile.h>
#include <taglib/id3v2tag.h>
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
namespace MetaData
{
std::vector<std::string>
getPropertyValuesFirstMatch(const TagLib::PropertyMap& properties, const std::set<std::string>& keys)
{
std::vector<std::string> res;
for (const std::string& key : keys)
{
const TagLib::StringList& values {properties[key]};
if (values.isEmpty())
continue;
res.reserve(values.size());
std::transform(std::cbegin(values), std::cend(values), std::back_inserter(res), [](const auto& value) { return stringTrim(value.to8Bit(true)); });
break;
}
return res;
}
std::vector<std::string>
getPropertyValues(const TagLib::PropertyMap& properties, const std::string& key)
{
return getPropertyValuesFirstMatch(properties, {std::move(key)});
}
static
std::vector<std::string>
splitAndTrimString(const std::string& str, const std::string& delimiters)
{
std::vector<std::string> res;
std::vector<std::string> strings {splitString(str, delimiters)};
for (const std::string& s : strings)
res.emplace_back(stringTrim(s));
return res;
}
static
std::vector<Artist>
getArtists(const TagLib::PropertyMap& properties)
{
std::vector<Artist> res;
std::vector<std::string> artistNames {getPropertyValues(properties, "ARTISTS")};
if (artistNames.empty())
artistNames = getPropertyValues(properties, "ARTIST");
if (artistNames.empty())
return res;
const std::vector<std::string> artistsMBID {getPropertyValuesFirstMatch(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})};
if (artistNames.size() == artistsMBID.size())
{
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::cbegin(artistsMBID), std::back_inserter(res),
[&](const std::string& name, const std::string& mbid) { return Artist{name, mbid}; });
}
else
{
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(res),
[&](const std::string& name) { return Artist{name, ""}; });
}
return res;
}
static
std::vector<Artist>
getAlbumArtists(const TagLib::PropertyMap& properties)
{
std::vector<Artist> res;
std::vector<std::string> artistNames {getPropertyValues(properties, "ALBUMARTIST")};
if (artistNames.empty())
return res;
const std::vector<std::string> artistsMBID {getPropertyValuesFirstMatch(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})};
if (artistNames.size() == artistsMBID.size())
{
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::cbegin(artistsMBID), std::back_inserter(res),
[&](const std::string& name, const std::string& mbid) { return Artist{name, mbid}; });
}
else
{
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(res),
[&](const std::string& name) { return Artist{name, ""}; });
}
return res;
}
static
boost::optional<Album>
getAlbum(const TagLib::PropertyMap& properties)
{
boost::optional<Album> res;
std::vector<std::string> albumName {getPropertyValues(properties, "ALBUM")};
if (albumName.empty())
return res;
std::vector<std::string> albumMBID {getPropertyValuesFirstMatch(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID"})};
res = Album{std::move(albumName.front()), ""};
if (!albumMBID.empty())
res->musicBrainzAlbumID = std::move(albumMBID.front());
return res;
}
boost::optional<Track>
TagLibParser::parse(const boost::filesystem::path& p, bool debug)
{
TagLib::FileRef f {p.string().c_str(),
true, // read audio properties
TagLib::AudioProperties::Fast};
if (f.isNull())
{
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
return boost::none;
}
if (!f.audioProperties())
{
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
return boost::none;
}
Track track;
{
const TagLib::AudioProperties *properties {f.audioProperties() };
track.duration = std::chrono::milliseconds {properties->length() * 1000};
MetaData::AudioStream audioStream {.bitRate = static_cast<unsigned>(properties->bitrate() * 1000)};
track.audioStreams = {std::move(audioStream)};
}
// Not that good embedded pictures handling
// MP3
if (TagLib::MPEG::File *mp3File {dynamic_cast<TagLib::MPEG::File*>(f.file())})
{
if (mp3File->ID3v2Tag())
{
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
track.hasCover = true;
}
}
if (f.tag())
{
MetaData::Clusters clusters;
const TagLib::PropertyMap& properties {f.file()->properties()};
for(const auto& property : properties)
{
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
// TODO validate MBID format
if (debug)
{
std::vector<std::string> strs;
std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); });
std::cout << "[" << tag << "] = " << joinStrings(strs, "*SEP*") << std::endl;
}
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
continue;
std::string value {stringTrim(values.front().to8Bit(true))};
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
{
track.musicBrainzTrackID = value;
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID")
track.musicBrainzRecordID = value;
else if (tag == "ACOUSTID_ID")
track.acoustID = value;
else if (tag == "TRACKTOTAL")
{
auto totalTrack = readAs<std::size_t>(value);
if (totalTrack)
track.totalTrack = totalTrack;
}
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {splitAndTrimString(value, "/")};
if (!strings.empty())
{
track.trackNumber = readAs<std::size_t>(strings[0]);
// Lower priority than TRACKTOTAL
if (strings.size() > 1 && !track.totalTrack)
track.totalTrack = readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DISCTOTAL")
{
auto totalDisc = readAs<std::size_t>(value);
if (totalDisc)
track.totalDisc = totalDisc;
}
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {splitString(value, "/")};
if (!strings.empty())
{
track.discNumber = readAs<std::size_t>(strings[0]);
// Lower priority than DISCTOTAL
if (strings.size() > 1 && !track.totalDisc)
track.totalDisc = readAs<std::size_t>(strings[1]);
}
}
else if (tag == "DATE")
track.year = readAs<int>(value);
else if (tag == "ORIGINALDATE" && !track.originalYear)
{
// Lower priority than ORIGINALYEAR
track.originalYear = readAs<int>(value);
}
else if (tag == "ORIGINALYEAR")
{
// Higher priority than ORIGINALDATE
auto originalYear = readAs<int>(value);
if (originalYear)
track.originalYear = originalYear;
}
else if (tag == "METADATA_BLOCK_PICTURE")
track.hasCover = true;
else if (tag == "COPYRIGHT")
track.copyright = value;
else if (tag == "COPYRIGHTURL")
track.copyrightURL = value;
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
for (const auto& valueList : values)
{
auto values = splitAndTrimString(valueList.to8Bit(true), "/,;");
for (const auto& value : values)
clusterNames.insert(value);
}
if (!clusterNames.empty())
track.clusters[tag] = clusterNames;
}
}
track.artists = getArtists(properties);
track.albumArtists = getAlbumArtists(properties);
track.album = getAlbum(properties);
}
return track;
}
} // namespace MetaData