2015-09-06 19:57:33 +02:00
|
|
|
#include "./id3v1tag.h"
|
|
|
|
#include "./id3genres.h"
|
2015-09-06 15:42:18 +02:00
|
|
|
|
2018-03-05 17:49:29 +01:00
|
|
|
#include "../diagnostics.h"
|
2018-03-07 01:17:50 +01:00
|
|
|
#include "../exceptions.h"
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
#include <c++utilities/conversion/conversionexception.h>
|
2019-06-01 22:36:08 +02:00
|
|
|
#include <c++utilities/conversion/stringbuilder.h>
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
#include <cstring>
|
2019-06-01 22:36:08 +02:00
|
|
|
#include <initializer_list>
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
using namespace std;
|
2019-06-10 22:49:11 +02:00
|
|
|
using namespace CppUtilities;
|
2015-04-22 19:22:01 +02:00
|
|
|
|
2018-03-06 23:09:15 +01:00
|
|
|
namespace TagParser {
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
/*!
|
2018-06-03 20:38:32 +02:00
|
|
|
* \class TagParser::Id3v1Tag
|
|
|
|
* \brief Implementation of TagParser::Tag for ID3v1 tags.
|
2015-04-22 19:22:01 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Constructs a new tag.
|
|
|
|
*/
|
|
|
|
Id3v1Tag::Id3v1Tag()
|
2018-03-07 01:17:50 +01:00
|
|
|
{
|
|
|
|
}
|
2015-04-22 19:22:01 +02:00
|
|
|
|
|
|
|
TagType Id3v1Tag::type() const
|
|
|
|
{
|
|
|
|
return TagType::Id3v1Tag;
|
|
|
|
}
|
|
|
|
|
2021-01-30 21:53:06 +01:00
|
|
|
std::string_view Id3v1Tag::typeName() const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2017-03-07 00:02:59 +01:00
|
|
|
return tagName;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
2019-06-14 18:07:59 +02:00
|
|
|
/*!
|
|
|
|
* \brief Returns only true for TagTextEncoding::Latin1.
|
|
|
|
* \remarks
|
|
|
|
* The encoding to be used within ID3v1 tags is not standardized but it seems that Latin-1 is the most
|
|
|
|
* commonly used character set and hence safest to use. Hence that is the only encoding which can be safely
|
|
|
|
* recommended here. Despite that, the Id3v1Tag class is actually able to deal with UTF-8 as well. It will
|
|
|
|
* use the BOM to detect and serialize UTF-8.
|
|
|
|
*/
|
2015-04-22 19:22:01 +02:00
|
|
|
bool Id3v1Tag::canEncodingBeUsed(TagTextEncoding encoding) const
|
|
|
|
{
|
2019-06-14 18:07:59 +02:00
|
|
|
return encoding == TagTextEncoding::Latin1;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Parses tag information from the specified \a stream.
|
|
|
|
* \throws Throws std::ios_base::failure when an IO error occurs.
|
2018-06-03 20:38:32 +02:00
|
|
|
* \throws Throws TagParser::Failure or a derived exception when a parsing
|
2015-04-22 19:22:01 +02:00
|
|
|
* error occurs.
|
|
|
|
*/
|
2018-03-05 17:49:29 +01:00
|
|
|
void Id3v1Tag::parse(std::istream &stream, Diagnostics &diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2019-06-12 20:40:45 +02:00
|
|
|
CPP_UTILITIES_UNUSED(diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
char buffer[128];
|
|
|
|
stream.read(buffer, 128);
|
2018-03-07 01:17:50 +01:00
|
|
|
if (buffer[0] != 0x54 || buffer[1] != 0x41 || buffer[2] != 0x47) {
|
2015-04-22 19:22:01 +02:00
|
|
|
throw NoDataFoundException();
|
|
|
|
}
|
2018-03-05 17:49:29 +01:00
|
|
|
m_size = 128;
|
|
|
|
readValue(m_title, 30, buffer + 3);
|
|
|
|
readValue(m_artist, 30, buffer + 33);
|
|
|
|
readValue(m_album, 30, buffer + 63);
|
|
|
|
readValue(m_year, 4, buffer + 93);
|
2019-06-01 22:36:08 +02:00
|
|
|
const auto is11 = buffer[125] == 0;
|
|
|
|
if (is11) {
|
2018-03-05 17:49:29 +01:00
|
|
|
readValue(m_comment, 28, buffer + 97);
|
|
|
|
m_version = "1.1";
|
|
|
|
} else {
|
|
|
|
readValue(m_comment, 30, buffer + 97);
|
|
|
|
m_version = "1.0";
|
|
|
|
}
|
2019-06-01 22:36:08 +02:00
|
|
|
readValue(m_comment, is11 ? 28 : 30, buffer + 97);
|
|
|
|
if (is11) {
|
2018-03-05 17:49:29 +01:00
|
|
|
m_trackPos.assignPosition(PositionInSet(*reinterpret_cast<char *>(buffer + 126), 0));
|
|
|
|
}
|
|
|
|
m_genre.assignStandardGenreIndex(*reinterpret_cast<unsigned char *>(buffer + 127));
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Writes tag information to the specified \a stream.
|
|
|
|
*
|
|
|
|
* \throws Throws std::ios_base::failure when an IO error occurs.
|
2018-06-03 20:38:32 +02:00
|
|
|
* \throws Throws TagParser::Failure or a derived exception when a making
|
2015-04-22 19:22:01 +02:00
|
|
|
* error occurs.
|
|
|
|
*/
|
2018-03-05 17:49:29 +01:00
|
|
|
void Id3v1Tag::make(ostream &stream, Diagnostics &diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
|
|
|
static const string context("making ID3v1 tag");
|
|
|
|
char buffer[30];
|
|
|
|
buffer[0] = 0x54;
|
|
|
|
buffer[1] = 0x41;
|
|
|
|
buffer[2] = 0x47;
|
|
|
|
stream.write(buffer, 3);
|
2018-05-31 00:25:32 +02:00
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
// write text fields
|
2018-03-05 17:49:29 +01:00
|
|
|
writeValue(m_title, 30, buffer, stream, diag);
|
|
|
|
writeValue(m_artist, 30, buffer, stream, diag);
|
|
|
|
writeValue(m_album, 30, buffer, stream, diag);
|
|
|
|
writeValue(m_year, 4, buffer, stream, diag);
|
|
|
|
writeValue(m_comment, 28, buffer, stream, diag);
|
2018-05-31 00:25:32 +02:00
|
|
|
|
|
|
|
// set "default" values for numeric fields
|
2015-04-22 19:22:01 +02:00
|
|
|
buffer[0] = 0x0; // empty byte
|
2018-05-31 00:25:32 +02:00
|
|
|
buffer[1] = 0x0; // track number
|
2015-04-22 19:22:01 +02:00
|
|
|
buffer[2] = 0x0; // genre
|
2018-05-31 00:25:32 +02:00
|
|
|
|
|
|
|
// write track
|
2018-05-31 00:21:54 +02:00
|
|
|
if (!m_trackPos.isEmpty()) {
|
2018-03-07 01:11:42 +01:00
|
|
|
try {
|
2018-05-31 00:25:32 +02:00
|
|
|
const auto position(m_trackPos.toPositionInSet().position());
|
|
|
|
if (position < 0x00 || position > 0xFF) {
|
|
|
|
throw ConversionException();
|
|
|
|
}
|
|
|
|
buffer[1] = static_cast<char>(position);
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const ConversionException &) {
|
|
|
|
diag.emplace_back(
|
|
|
|
DiagLevel::Warning, "Track position field can not be set because given value can not be converted appropriately.", context);
|
2018-03-07 01:11:42 +01:00
|
|
|
}
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
2018-05-31 00:25:32 +02:00
|
|
|
|
|
|
|
// write genre
|
2015-04-22 19:22:01 +02:00
|
|
|
try {
|
2018-05-31 00:25:32 +02:00
|
|
|
const auto genreIndex(m_genre.toStandardGenreIndex());
|
|
|
|
if (genreIndex < 0x00 || genreIndex > 0xFF) {
|
|
|
|
throw ConversionException();
|
|
|
|
}
|
|
|
|
buffer[2] = static_cast<char>(genreIndex);
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const ConversionException &) {
|
2018-05-31 00:25:32 +02:00
|
|
|
diag.emplace_back(DiagLevel::Warning,
|
|
|
|
"Genre field can not be set because given value can not be converted to a standard genre number supported by ID3v1.", context);
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
2018-05-31 00:25:32 +02:00
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
stream.write(buffer, 3);
|
|
|
|
stream.flush();
|
|
|
|
}
|
|
|
|
|
|
|
|
const TagValue &Id3v1Tag::value(KnownField field) const
|
|
|
|
{
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (field) {
|
2015-04-22 19:22:01 +02:00
|
|
|
case KnownField::Title:
|
|
|
|
return m_title;
|
|
|
|
case KnownField::Artist:
|
|
|
|
return m_artist;
|
|
|
|
case KnownField::Album:
|
|
|
|
return m_album;
|
2020-04-22 23:54:10 +02:00
|
|
|
case KnownField::RecordDate:
|
2015-04-22 19:22:01 +02:00
|
|
|
return m_year;
|
|
|
|
case KnownField::Comment:
|
|
|
|
return m_comment;
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
return m_trackPos;
|
|
|
|
case KnownField::Genre:
|
|
|
|
return m_genre;
|
|
|
|
default:
|
|
|
|
return TagValue::empty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Id3v1Tag::setValue(KnownField field, const TagValue &value)
|
|
|
|
{
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (field) {
|
2015-04-22 19:22:01 +02:00
|
|
|
case KnownField::Title:
|
|
|
|
m_title = value;
|
|
|
|
break;
|
|
|
|
case KnownField::Artist:
|
|
|
|
m_artist = value;
|
|
|
|
break;
|
|
|
|
case KnownField::Album:
|
|
|
|
m_album = value;
|
|
|
|
break;
|
2020-04-22 23:54:10 +02:00
|
|
|
case KnownField::RecordDate:
|
2015-04-22 19:22:01 +02:00
|
|
|
m_year = value;
|
|
|
|
break;
|
|
|
|
case KnownField::Comment:
|
|
|
|
m_comment = value;
|
|
|
|
break;
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
m_trackPos = value;
|
|
|
|
break;
|
|
|
|
case KnownField::Genre:
|
|
|
|
m_genre = value;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Id3v1Tag::setValueConsideringTypeInfo(KnownField field, const TagValue &value, const string &)
|
|
|
|
{
|
|
|
|
return setValue(field, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Id3v1Tag::hasField(KnownField field) const
|
|
|
|
{
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (field) {
|
2015-04-22 19:22:01 +02:00
|
|
|
case KnownField::Title:
|
|
|
|
return !m_title.isEmpty();
|
|
|
|
case KnownField::Artist:
|
|
|
|
return !m_artist.isEmpty();
|
|
|
|
case KnownField::Album:
|
|
|
|
return !m_album.isEmpty();
|
|
|
|
return !m_year.isEmpty();
|
|
|
|
case KnownField::Comment:
|
|
|
|
return !m_comment.isEmpty();
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
return !m_trackPos.isEmpty();
|
|
|
|
case KnownField::Genre:
|
|
|
|
return !m_genre.isEmpty();
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Id3v1Tag::removeAllFields()
|
|
|
|
{
|
|
|
|
m_title.clearDataAndMetadata();
|
|
|
|
m_artist.clearDataAndMetadata();
|
|
|
|
m_album.clearDataAndMetadata();
|
|
|
|
m_year.clearDataAndMetadata();
|
|
|
|
m_comment.clearDataAndMetadata();
|
|
|
|
m_trackPos.clearDataAndMetadata();
|
|
|
|
m_genre.clearDataAndMetadata();
|
|
|
|
}
|
|
|
|
|
2021-03-20 21:26:25 +01:00
|
|
|
std::size_t Id3v1Tag::fieldCount() const
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2021-03-20 21:26:25 +01:00
|
|
|
auto count = std::size_t(0);
|
2020-12-05 20:48:57 +01:00
|
|
|
for (const auto &value : std::initializer_list<const TagValue *>{ &m_title, &m_artist, &m_album, &m_year, &m_comment, &m_trackPos, &m_genre }) {
|
|
|
|
if (!value->isEmpty()) {
|
2015-04-22 19:22:01 +02:00
|
|
|
++count;
|
2015-08-16 23:39:42 +02:00
|
|
|
}
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Id3v1Tag::supportsField(KnownField field) const
|
|
|
|
{
|
2018-03-07 01:17:50 +01:00
|
|
|
switch (field) {
|
2015-04-22 19:22:01 +02:00
|
|
|
case KnownField::Title:
|
|
|
|
case KnownField::Artist:
|
|
|
|
case KnownField::Album:
|
2020-04-22 23:54:10 +02:00
|
|
|
case KnownField::RecordDate:
|
2015-04-22 19:22:01 +02:00
|
|
|
case KnownField::Comment:
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
case KnownField::Genre:
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-05 01:46:31 +02:00
|
|
|
void Id3v1Tag::ensureTextValuesAreProperlyEncoded()
|
|
|
|
{
|
2019-06-01 22:36:08 +02:00
|
|
|
for (auto *value : initializer_list<TagValue *>{ &m_title, &m_artist, &m_album, &m_year, &m_comment, &m_trackPos, &m_genre }) {
|
|
|
|
// convert UTF-16 to UTF-8
|
|
|
|
switch (value->dataEncoding()) {
|
|
|
|
case TagTextEncoding::Latin1:
|
|
|
|
case TagTextEncoding::Utf8:
|
|
|
|
case TagTextEncoding::Unspecified:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
value->convertDataEncoding(TagTextEncoding::Utf8);
|
|
|
|
}
|
|
|
|
}
|
2016-08-05 01:46:31 +02:00
|
|
|
}
|
|
|
|
|
2015-04-22 19:22:01 +02:00
|
|
|
/*!
|
2016-08-05 01:46:31 +02:00
|
|
|
* \brief Internally used to read values with the specified \a maxLength from the specified \a buffer.
|
2015-04-22 19:22:01 +02:00
|
|
|
*/
|
2016-08-05 01:46:31 +02:00
|
|
|
void Id3v1Tag::readValue(TagValue &value, size_t maxLength, const char *buffer)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2016-08-05 01:46:31 +02:00
|
|
|
const char *end = buffer + maxLength - 1;
|
2021-05-09 12:15:00 +02:00
|
|
|
while ((*end == 0x0 || *end == ' ') && end >= buffer) {
|
2015-04-22 19:22:01 +02:00
|
|
|
--end;
|
2016-08-05 01:46:31 +02:00
|
|
|
--maxLength;
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
2021-05-09 12:15:00 +02:00
|
|
|
if (buffer == end) {
|
|
|
|
return;
|
|
|
|
}
|
2019-06-10 22:49:11 +02:00
|
|
|
if (maxLength >= 3 && BE::toUInt24(buffer) == 0x00EFBBBF) {
|
2019-06-01 22:36:08 +02:00
|
|
|
value.assignData(buffer + 3, maxLength - 3, TagDataType::Text, TagTextEncoding::Utf8);
|
|
|
|
} else {
|
|
|
|
value.assignData(buffer, maxLength, TagDataType::Text, TagTextEncoding::Latin1);
|
|
|
|
}
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Internally used to write values.
|
|
|
|
*/
|
2018-03-05 17:49:29 +01:00
|
|
|
void Id3v1Tag::writeValue(const TagValue &value, size_t length, char *buffer, ostream &targetStream, Diagnostics &diag)
|
2015-04-22 19:22:01 +02:00
|
|
|
{
|
2019-06-01 22:36:08 +02:00
|
|
|
// initialize buffer with zeroes
|
2015-04-22 19:22:01 +02:00
|
|
|
memset(buffer, 0, length);
|
2019-06-01 22:36:08 +02:00
|
|
|
|
|
|
|
// stringify value
|
|
|
|
string valueAsString;
|
2015-04-22 19:22:01 +02:00
|
|
|
try {
|
2019-06-01 22:36:08 +02:00
|
|
|
valueAsString = value.toString();
|
2018-03-07 01:17:50 +01:00
|
|
|
} catch (const ConversionException &) {
|
|
|
|
diag.emplace_back(
|
|
|
|
DiagLevel::Warning, "Field can not be set because given value can not be converted appropriately.", "making ID3v1 tag field");
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
2019-06-01 22:36:08 +02:00
|
|
|
|
|
|
|
// handle encoding
|
|
|
|
auto *valueStart = buffer;
|
|
|
|
auto valueLength = length;
|
2021-08-08 01:20:39 +02:00
|
|
|
auto hasProblematicEncoding = false;
|
2019-06-01 22:36:08 +02:00
|
|
|
switch (value.dataEncoding()) {
|
|
|
|
case TagTextEncoding::Latin1:
|
|
|
|
break;
|
|
|
|
case TagTextEncoding::Utf8:
|
2021-08-08 01:20:39 +02:00
|
|
|
// write UTF-8 BOM if the value contains non-ASCII characters
|
2019-06-01 22:36:08 +02:00
|
|
|
for (const auto c : valueAsString) {
|
|
|
|
if ((c & 0x80) == 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
buffer[0] = static_cast<char>(0xEF);
|
|
|
|
buffer[1] = static_cast<char>(0xBB);
|
|
|
|
buffer[2] = static_cast<char>(0xBF);
|
|
|
|
valueStart += 3;
|
|
|
|
valueLength -= 3;
|
2021-08-08 01:20:39 +02:00
|
|
|
hasProblematicEncoding = true;
|
2019-06-01 22:36:08 +02:00
|
|
|
break;
|
|
|
|
}
|
2021-08-08 01:20:39 +02:00
|
|
|
break;
|
2019-06-01 22:36:08 +02:00
|
|
|
default:
|
2021-08-08 01:20:39 +02:00
|
|
|
hasProblematicEncoding = true;
|
|
|
|
}
|
|
|
|
if (hasProblematicEncoding) {
|
2019-06-01 22:36:08 +02:00
|
|
|
diag.emplace_back(DiagLevel::Warning, "The used encoding is unlikely to be supported by other software.", "making ID3v1 tag field");
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy the string
|
|
|
|
if (valueAsString.size() > length) {
|
|
|
|
diag.emplace_back(
|
|
|
|
DiagLevel::Warning, argsToString("Value has been truncated. Max. ", length, " characters supported."), "making ID3v1 tag field");
|
|
|
|
}
|
|
|
|
valueAsString.copy(valueStart, valueLength);
|
|
|
|
|
|
|
|
targetStream.write(buffer, static_cast<streamsize>(length));
|
2015-04-22 19:22:01 +02:00
|
|
|
}
|
|
|
|
|
2018-03-07 01:17:50 +01:00
|
|
|
} // namespace TagParser
|