Compare commits
90 Commits
Author | SHA1 | Date |
---|---|---|
Martchus | acfb9ef219 | |
Martchus | 351e953b83 | |
Martchus | 8204b2dfde | |
Martchus | 8246d30ec5 | |
Martchus | d48722f26c | |
Martchus | 45ab6b17b9 | |
Martchus | 6fb34ec3b3 | |
Martchus | 33327390e9 | |
Martchus | 7f3d4c5751 | |
Martchus | b1bca85ef4 | |
Martchus | 0f669c88a7 | |
Martchus | 1df871870b | |
Martchus | 909a3ee98a | |
Martchus | 5aef3f84ee | |
Martchus | 56ccd3da80 | |
Martchus | 9f41c30443 | |
Martchus | ef0ab3d8c3 | |
Martchus | 0827002183 | |
Martchus | f2e97b9899 | |
Martchus | db5e1f2c8c | |
Martchus | 04795f957f | |
Martchus | c0e9f9bf83 | |
Martchus | e6bb98d6e6 | |
Martchus | f5497fb300 | |
Martchus | f7941d442f | |
Martchus | 03f9698269 | |
Martchus | 8a6cffad95 | |
Martchus | 54a87cd32c | |
Martchus | 92345027fb | |
Martchus | 90cace1e95 | |
Martchus | 6eab8b8718 | |
Martchus | a5ab3ed1b2 | |
Martchus | 5745632af7 | |
Martchus | 6f321b7b00 | |
Martchus | 60385aa347 | |
Martchus | c5cd20682d | |
Martchus | 8ad28f857b | |
Martchus | 6ed968f5e6 | |
Martchus | a167e0702e | |
Martchus | 405631625f | |
Martchus | 6fb16d72eb | |
Martchus | 111d6190cb | |
Martchus | 0a2b948f26 | |
Martchus | 9e4d221bb1 | |
Martchus | ea6b474e8c | |
Martchus | 10a6b10658 | |
Martchus | 25166e4cb4 | |
Martchus | 7d9d5c8408 | |
Martchus | 21edf75047 | |
Martchus | d80736743b | |
Martchus | 981db492e4 | |
Martchus | 15d74bbfb0 | |
Martchus | 3272d9c511 | |
Martchus | 522aa4359e | |
Martchus | cb93d6900c | |
Martchus | a9c4bca679 | |
Martchus | ca6abe31a0 | |
Martchus | 205b119416 | |
Martchus | 338011e3eb | |
Martchus | 7873db611a | |
Martchus | dc4e4082e0 | |
Martchus | 937631e5c4 | |
Martchus | 6a07d60649 | |
Martchus | edc7aa06c9 | |
Martchus | 04e6996ce3 | |
Martchus | 766e7657b5 | |
Martchus | 8d4c315611 | |
Martchus | 3193df8e3f | |
Martchus | 0347ca73eb | |
Martchus | dbdfd015bb | |
Martchus | 93da1f1e25 | |
Martchus | bfe6ce8c1e | |
Martchus | 49c6b61e0c | |
Martchus | 6333aaa84b | |
Martchus | 4ac97910e9 | |
Martchus | 0cc9271a7f | |
Martchus | b50de3cf4f | |
Martchus | f0d8a6efa7 | |
Martchus | 74f6d2b6ac | |
Martchus | 4aff37b788 | |
Martchus | d6a2903749 | |
Martchus | 99bb786eeb | |
Martchus | 6da62db035 | |
Martchus | 63c76e6ca4 | |
Martchus | f068c44172 | |
Martchus | df27013c2f | |
Martchus | 5dd5e301b3 | |
Martchus | 2484cb03f8 | |
Martchus | 60233c2af2 | |
Martchus | b4d9a3aeb8 |
|
@ -42,3 +42,7 @@ Makefile*
|
|||
|
||||
# clang-format
|
||||
/.clang-format
|
||||
|
||||
# file with language codes one might store in the a local checkout
|
||||
iso_639-2.json
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.3.0 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR)
|
||||
|
||||
# meta data
|
||||
project(tagparser)
|
||||
|
@ -8,8 +8,8 @@ set(META_APP_NAME "Tag Parser")
|
|||
set(META_APP_AUTHOR "Martchus")
|
||||
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
|
||||
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags")
|
||||
set(META_VERSION_MAJOR 11)
|
||||
set(META_VERSION_MINOR 4)
|
||||
set(META_VERSION_MAJOR 12)
|
||||
set(META_VERSION_MINOR 2)
|
||||
set(META_VERSION_PATCH 0)
|
||||
set(META_REQUIRED_CPP_UNIT_VERSION 1.14.0)
|
||||
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)
|
||||
|
@ -185,8 +185,9 @@ set(RES_FILES "${LANGUAGE_HEADER_ISO_639_2}")
|
|||
set(CONFIGURATION_PACKAGE_SUFFIX
|
||||
""
|
||||
CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities")
|
||||
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.13.0 REQUIRED)
|
||||
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.21.0 REQUIRED)
|
||||
use_cpp_utilities(VISIBILITY PUBLIC)
|
||||
list(APPEND META_PRIVATE_COMPILE_DEFINITIONS CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS)
|
||||
|
||||
# link against a possibly required extra library for std::filesystem
|
||||
use_standard_filesystem()
|
||||
|
|
12
README.md
12
README.md
|
@ -7,11 +7,19 @@ The tag library can read and write the following tag formats:
|
|||
* iTunes-style MP4/M4A tags (MP4-DASH is supported)
|
||||
* ID3v1 and ID3v2 tags
|
||||
* conversion between ID3v1 and different versions of ID3v2 is possible
|
||||
* mainly for use in MP3 files but can be added to any kind of file
|
||||
* Vorbis, Opus and FLAC comments in Ogg streams
|
||||
* cover art via "METADATA_BLOCK_PICTURE" is supported
|
||||
* Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams
|
||||
* Matroska/WebM tags and attachments
|
||||
|
||||
Further remarks:
|
||||
|
||||
* Unsupported file contents (such as unsupported tag formats) are *generally* preserved as-is.
|
||||
* Note that APE tags are *not* supported. APE tags in the beginning of a file are strongly
|
||||
unrecommended and thus discarded when applying changes. APE tags at the end of the file
|
||||
are preserved as-is when applying changes.
|
||||
|
||||
## File layout options
|
||||
### Tag position
|
||||
The library allows you to choose whether tags should be placed at the beginning or at
|
||||
|
@ -59,7 +67,7 @@ The library is aware of different text encodings and can convert between differe
|
|||
|
||||
### Further documentation
|
||||
For more examples check out the command line interface of [Tag Editor](https://github.com/Martchus/tageditor).
|
||||
API documentation can be generated using Doxygen with `make tagparser_apidoc`.
|
||||
API documentation can be generated using Doxygen with `cmake --build … --target tagparser_apidoc`.
|
||||
|
||||
## Bugs, stability
|
||||
Bugs can be reported on GitHub.
|
||||
|
@ -86,6 +94,6 @@ the ["Building this straight"](https://github.com/Martchus/tageditor#building-th
|
|||
More TODOs are tracked in the [issue section at GitHub](https://github.com/Martchus/tagparser/issues).
|
||||
|
||||
## Copyright notice and license
|
||||
Copyright © 2015-2022 Marius Kittler
|
||||
Copyright © 2015-2024 Marius Kittler
|
||||
|
||||
All code is licensed under [GPL-2-or-later](LICENSE).
|
||||
|
|
|
@ -955,7 +955,7 @@ std::int16_t AacFrameElementParser::sbrHuffmanDec(SbrHuffTab table)
|
|||
|
||||
void AacFrameElementParser::parseSbrGrid(std::shared_ptr<AacSbrInfo> &sbr, std::uint8_t channel)
|
||||
{
|
||||
std::uint8_t tmp, bsEnvCount;
|
||||
std::uint8_t tmp, bsEnvCount = 0;
|
||||
//byte bsRelCount0, bsRelCount1;
|
||||
switch ((sbr->bsFrameClass[channel] = m_reader.readBits<std::uint8_t>(2))) {
|
||||
using namespace BsFrameClasses;
|
||||
|
|
|
@ -14,6 +14,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractAttachmentPrivate struct contains private fields of the AbstractAttachment class.
|
||||
struct AbstractAttachmentPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::StreamDataBlock
|
||||
* \brief The StreamDataBlock class is a reference to a certain data block of a stream.
|
||||
|
@ -127,6 +130,23 @@ FileDataBlock::~FileDataBlock()
|
|||
* \brief The AbstractAttachment class parses and stores attachment information.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new attachment.
|
||||
*/
|
||||
AbstractAttachment::AbstractAttachment()
|
||||
: m_id(0)
|
||||
, m_isDataFromFile(false)
|
||||
, m_ignored(false)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the attachment.
|
||||
*/
|
||||
AbstractAttachment::~AbstractAttachment()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns a label for the track.
|
||||
*/
|
||||
|
@ -180,7 +200,7 @@ void AbstractAttachment::setFile(string_view path, Diagnostics &diag, AbortableP
|
|||
if (!mimeType.empty()) {
|
||||
m_mimeType = mimeType;
|
||||
}
|
||||
m_data = move(file);
|
||||
m_data = std::move(file);
|
||||
m_isDataFromFile = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,8 @@ inline const MediaFileInfo *FileDataBlock::fileInfo() const
|
|||
return m_fileInfo.get();
|
||||
}
|
||||
|
||||
struct AbstractAttachmentPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractAttachment {
|
||||
public:
|
||||
const std::string &description() const;
|
||||
|
@ -123,7 +125,8 @@ public:
|
|||
bool isEmpty() const;
|
||||
|
||||
protected:
|
||||
AbstractAttachment();
|
||||
explicit AbstractAttachment();
|
||||
virtual ~AbstractAttachment();
|
||||
|
||||
private:
|
||||
std::string m_description;
|
||||
|
@ -131,20 +134,11 @@ private:
|
|||
std::string m_mimeType;
|
||||
std::uint64_t m_id;
|
||||
std::unique_ptr<StreamDataBlock> m_data;
|
||||
std::unique_ptr<AbstractAttachmentPrivate> m_p;
|
||||
bool m_isDataFromFile;
|
||||
bool m_ignored;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new attachment.
|
||||
*/
|
||||
inline AbstractAttachment::AbstractAttachment()
|
||||
: m_id(0)
|
||||
, m_isDataFromFile(false)
|
||||
, m_ignored(false)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns a description of the attachment.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractChapterPrivate struct contains private fields of the AbstractChapter class.
|
||||
struct AbstractChapterPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractChapter
|
||||
* \brief The AbstractChapter class parses chapter information.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -12,6 +13,7 @@ namespace TagParser {
|
|||
|
||||
class AbortableProgressFeedback;
|
||||
class Diagnostics;
|
||||
struct AbstractChapterPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractChapter {
|
||||
public:
|
||||
|
@ -41,6 +43,7 @@ protected:
|
|||
CppUtilities::TimeSpan m_startTime;
|
||||
CppUtilities::TimeSpan m_endTime;
|
||||
std::vector<std::uint64_t> m_tracks;
|
||||
std::unique_ptr<AbstractChapterPrivate> m_p;
|
||||
bool m_hidden;
|
||||
bool m_enabled;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,11 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractContainerPrivate struct contains private fields of the AbstractContainer class.
|
||||
struct AbstractContainerPrivate {
|
||||
std::vector<std::string> muxingApps, writingApps;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractContainer
|
||||
* \brief The AbstractContainer class provides an interface and common functionality to parse and make a certain container format.
|
||||
|
@ -472,6 +477,40 @@ bool AbstractContainer::supportsTitle() const
|
|||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the muxing applications specified as meta-data.
|
||||
*/
|
||||
const std::vector<std::string> &AbstractContainer::muxingApplications() const
|
||||
{
|
||||
static const auto empty = std::vector<std::string>();
|
||||
return m_p ? m_p->muxingApps : empty;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the muxing applications specified as meta-data.
|
||||
*/
|
||||
std::vector<std::string> &AbstractContainer::muxingApplications()
|
||||
{
|
||||
return p()->muxingApps;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the writing applications specified as meta-data.
|
||||
*/
|
||||
const std::vector<std::string> &AbstractContainer::writingApplications() const
|
||||
{
|
||||
static const auto empty = std::vector<std::string>();
|
||||
return m_p ? m_p->writingApps : empty;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the writing applications specified as meta-data.
|
||||
*/
|
||||
std::vector<std::string> &AbstractContainer::writingApplications()
|
||||
{
|
||||
return p()->writingApps;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the number of segments.
|
||||
*/
|
||||
|
@ -499,4 +538,15 @@ void AbstractContainer::reset()
|
|||
m_titles.clear();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the private data for the container.
|
||||
*/
|
||||
std::unique_ptr<AbstractContainerPrivate> &AbstractContainer::p()
|
||||
{
|
||||
if (!m_p) {
|
||||
m_p = std::make_unique<AbstractContainerPrivate>();
|
||||
}
|
||||
return m_p;
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <c++utilities/io/binarywriter.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
namespace CppUtilities {
|
||||
class BinaryReader;
|
||||
|
@ -25,6 +26,7 @@ class AbstractChapter;
|
|||
class AbstractAttachment;
|
||||
class Diagnostics;
|
||||
class AbortableProgressFeedback;
|
||||
struct AbstractContainerPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractContainer {
|
||||
public:
|
||||
|
@ -78,6 +80,8 @@ public:
|
|||
const std::vector<std::string> &titles() const;
|
||||
void setTitle(std::string_view title, std::size_t segmentIndex = 0);
|
||||
virtual bool supportsTitle() const;
|
||||
const std::vector<std::string> &muxingApplications() const;
|
||||
const std::vector<std::string> &writingApplications() const;
|
||||
virtual std::size_t segmentCount() const;
|
||||
CppUtilities::TimeSpan duration() const;
|
||||
CppUtilities::DateTime creationTime() const;
|
||||
|
@ -95,6 +99,8 @@ protected:
|
|||
virtual void internalParseChapters(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
virtual void internalParseAttachments(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
virtual void internalMakeFile(Diagnostics &diag, AbortableProgressFeedback &progress);
|
||||
std::vector<std::string> &muxingApplications();
|
||||
std::vector<std::string> &writingApplications();
|
||||
|
||||
std::uint64_t m_version;
|
||||
std::uint64_t m_readVersion;
|
||||
|
@ -115,10 +121,13 @@ protected:
|
|||
bool m_attachmentsParsed;
|
||||
|
||||
private:
|
||||
std::unique_ptr<AbstractContainerPrivate> &p();
|
||||
|
||||
std::uint64_t m_startOffset;
|
||||
std::iostream *m_stream;
|
||||
CppUtilities::BinaryReader m_reader;
|
||||
CppUtilities::BinaryWriter m_writer;
|
||||
std::unique_ptr<AbstractContainerPrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
|
|
@ -11,6 +11,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The AbstractTrackPrivate struct contains private fields of the AbstractTrack class.
|
||||
struct AbstractTrackPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::AbstractTrack
|
||||
* \brief The AbstractTrack class parses and stores technical information about
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <c++utilities/misc/flagenumclass.h>
|
||||
|
||||
#include <iosfwd>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
@ -104,6 +105,8 @@ CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(TagParser, TagParser::TrackFlags)
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
struct AbstractTrackPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT AbstractTrack {
|
||||
friend class MpegAudioFrameStream;
|
||||
friend class WaveAudioStream;
|
||||
|
@ -129,6 +132,7 @@ public:
|
|||
MediaType mediaType() const;
|
||||
std::string_view mediaTypeName() const;
|
||||
std::uint64_t size() const;
|
||||
void setSize(std::uint64_t size);
|
||||
std::uint32_t trackNumber() const;
|
||||
void setTrackNumber(std::uint32_t trackNumber);
|
||||
std::uint64_t id() const;
|
||||
|
@ -232,6 +236,7 @@ protected:
|
|||
AlphaMode m_alphaMode;
|
||||
DisplayUnit m_displayUnit;
|
||||
AspectRatioType m_aspectRatioType;
|
||||
std::unique_ptr<AbstractTrackPrivate> m_p;
|
||||
|
||||
private:
|
||||
std::string makeDescription(bool verbose) const;
|
||||
|
@ -392,6 +397,19 @@ inline std::uint64_t AbstractTrack::size() const
|
|||
return m_size;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the size in bytes.
|
||||
* \remarks
|
||||
* This is used by MediaFileInfo to set the track size for certain types of tracks before invoking the parsing.
|
||||
* If you use this a class derived from AbstractTrack directly you may want to do the same if not the entire
|
||||
* input stream is supposed to be considered part of the track and the parser would otherwise assume that (like
|
||||
* the parser of MpegAudioFrameStream might do).
|
||||
*/
|
||||
inline void AbstractTrack::setSize(std::uint64_t size)
|
||||
{
|
||||
m_size = size;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the track number if known; otherwise returns 0.
|
||||
*/
|
||||
|
|
|
@ -24,15 +24,8 @@ void AdtsStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedbac
|
|||
if (!m_istream) {
|
||||
throw NoDataFoundException();
|
||||
}
|
||||
// get size
|
||||
m_istream->seekg(-128, ios_base::end);
|
||||
if (m_reader.readUInt24BE() == 0x544147) {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) - 3u - m_startOffset;
|
||||
} else {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) + 125u - m_startOffset;
|
||||
}
|
||||
m_istream->seekg(static_cast<streamoff>(m_startOffset), ios_base::beg);
|
||||
// parse frame header
|
||||
m_istream->seekg(static_cast<std::streamoff>(m_startOffset), ios_base::beg);
|
||||
m_firstFrame.parseHeader(m_reader);
|
||||
m_format = Mpeg4AudioObjectIds::idToMediaFormat(m_firstFrame.mpeg4AudioObjectId());
|
||||
m_channelCount = Mpeg4ChannelConfigs::channelCount(m_channelConfig = m_firstFrame.mpeg4ChannelConfig());
|
||||
|
|
|
@ -2,13 +2,19 @@
|
|||
# generates C++ code for ISO-639-2 language codes
|
||||
cmake_minimum_required(VERSION 3.19.0 FATAL_ERROR)
|
||||
|
||||
# get language file if not specified
|
||||
if (NOT LANGUAGE_FILE)
|
||||
# default to path provided usually by iso-codecs package (https://salsa.debian.org/iso-codes-team/iso-codes)
|
||||
set(LANGUAGE_FILE "/usr/share/iso-codes/json/iso_639-2.json")
|
||||
# download the file from upstream repo if it is not installed locally
|
||||
if (NOT EXISTS "${LANGUAGE_FILE}")
|
||||
set(LANGUAGE_FILE "resources/iso_639-2.json")
|
||||
message(STATUS "Downloading ${LANGUAGE_FILE}")
|
||||
file(DOWNLOAD "https://salsa.debian.org/iso-codes-team/iso-codes/-/raw/main/data/iso_639-2.json" "${LANGUAGE_FILE}" SHOW_PROGRESS TLS_VERIFY ON)
|
||||
endif()
|
||||
endif ()
|
||||
if (NOT EXISTS "${LANGUAGE_FILE}")
|
||||
message(FATAL_ERROR "The file ${LANGUAGE_FILE} does not exist.")
|
||||
else()
|
||||
endif()
|
||||
if (NOT OUTPUT_PATH)
|
||||
message(FATAL_ERROR "No OUTPUT_PATH specified.")
|
||||
|
|
|
@ -42,12 +42,13 @@ void example()
|
|||
// code.
|
||||
// - Parsing a file can be expensive if the file is big or the disk IO is slow. You might want to
|
||||
// run it in a separate thread.
|
||||
// - At this point the parser does not make much use of the progress object.
|
||||
// - At this point the parser does not make much use of the progress object (in contrast to applyChanges()
|
||||
// shown below).
|
||||
fileInfo.parseContainerFormat(diag, progress);
|
||||
fileInfo.parseTags(diag, progress);
|
||||
fileInfo.parseAttachments(diag, progress);
|
||||
fileInfo.parseChapters(diag, progress);
|
||||
fileInfo.parseEverything(diag, progress); // just use that one if you want all over the above
|
||||
fileInfo.parseEverything(diag, progress); // just use that one if you want all of the above
|
||||
|
||||
// get tag as an object derived from the Tag class
|
||||
// notes:
|
||||
|
@ -55,7 +56,7 @@ void example()
|
|||
// fileInfo.createAppropriateTags(…) to create tags as needed.
|
||||
auto tag = fileInfo.tags().at(0);
|
||||
|
||||
// extract a field value and convert it to UTF-8 std::string (toString() might throw ConversionException)
|
||||
// extract a field value and convert it to a UTF-8 std::string (toString() might throw ConversionException)
|
||||
auto title = tag->value(TagParser::KnownField::Title).toString(TagParser::TagTextEncoding::Utf8);
|
||||
|
||||
// change a field value using an encoding suitable for the tag format
|
||||
|
@ -84,6 +85,5 @@ void example()
|
|||
// - Use progress.tryToAbort() from another thread or an interrupt handler to abort gracefully without leaving
|
||||
// the file in an inconsistent state.
|
||||
// - Be sure everything has been parsed before as the library needs to be aware of the whole file structure.
|
||||
fileInfo.parseEverything(diag, progress);
|
||||
fileInfo.applyChanges(diag, progress);
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ namespace TagParser {
|
|||
*
|
||||
* A template specialization for each FieldMapBasedTag subclass must be provided.
|
||||
*/
|
||||
template <typename ImplementationType> class FieldMapBasedTagTraits {
|
||||
};
|
||||
template <typename ImplementationType> class FieldMapBasedTagTraits {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::FieldMapBasedTag
|
||||
|
@ -185,7 +184,17 @@ template <class ImplementationType> inline std::vector<const TagValue *> FieldMa
|
|||
|
||||
template <class ImplementationType> inline bool FieldMapBasedTag<ImplementationType>::setValue(KnownField field, const TagValue &value)
|
||||
{
|
||||
return setValue(fieldId(field), value);
|
||||
const auto id = fieldId(field);
|
||||
if constexpr (std::is_arithmetic_v<IdentifierType>) {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (id.empty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return setValue(id, value);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -266,7 +275,17 @@ bool FieldMapBasedTag<ImplementationType>::setValues(const IdentifierType &id, c
|
|||
*/
|
||||
template <class ImplementationType> bool FieldMapBasedTag<ImplementationType>::setValues(KnownField field, const std::vector<TagValue> &values)
|
||||
{
|
||||
return setValues(fieldId(field), values);
|
||||
const auto id = fieldId(field);
|
||||
if constexpr (std::is_arithmetic_v<IdentifierType>) {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (id.empty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return setValues(id, values);
|
||||
}
|
||||
|
||||
template <class ImplementationType> inline bool FieldMapBasedTag<ImplementationType>::hasField(KnownField field) const
|
||||
|
|
|
@ -98,7 +98,7 @@ void FlacMetaDataBlockPicture::parse(istream &inputStream, std::uint32_t maxSize
|
|||
if (size) {
|
||||
auto data = make_unique<char[]>(size);
|
||||
inputStream.read(data.get(), size);
|
||||
m_value.assignData(move(data), size, TagDataType::Picture);
|
||||
m_value.assignData(std::move(data), size, TagDataType::Picture);
|
||||
} else {
|
||||
m_value.clearData();
|
||||
}
|
||||
|
|
|
@ -115,7 +115,11 @@ void FlacStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedbac
|
|||
m_vorbisComment = make_unique<VorbisComment>();
|
||||
}
|
||||
try {
|
||||
m_vorbisComment->parse(*m_istream, header.dataSize(), VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
auto flags = VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte;
|
||||
if (m_mediaFileInfo.fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
|
||||
flags += VorbisCommentFlags::ConvertTotalFields;
|
||||
}
|
||||
m_vorbisComment->parse(*m_istream, header.dataSize(), flags, diag);
|
||||
} catch (const Failure &) {
|
||||
// error is logged via notifications, just continue with the next metadata block
|
||||
}
|
||||
|
@ -139,7 +143,7 @@ void FlacStream::internalParseHeader(Diagnostics &diag, AbortableProgressFeedbac
|
|||
m_vorbisComment = make_unique<VorbisComment>();
|
||||
m_vorbisComment->setVendor(TagValue(APP_NAME " v" APP_VERSION, TagTextEncoding::Utf8));
|
||||
}
|
||||
m_vorbisComment->fields().insert(make_pair(coverField.id(), move(coverField)));
|
||||
m_vorbisComment->fields().insert(make_pair(coverField.id(), std::move(coverField)));
|
||||
}
|
||||
|
||||
} catch (const TruncatedDataException &) {
|
||||
|
|
|
@ -27,7 +27,7 @@ void FlacToOggMappingHeader::parseHeader(OggIterator &iterator)
|
|||
constexpr auto idSize = 0x05, mappingHeaderSize = 0x0D, blockHeaderSize = 0x04, streamInfoSize = 0x22;
|
||||
char buff[mappingHeaderSize + blockHeaderSize + streamInfoSize - idSize];
|
||||
iterator.read(buff, idSize);
|
||||
if (*buff != 0x7Fu || BE::toUInt32(buff + 1) != 0x464C4143u) {
|
||||
if (*buff != 0x7Fu || BE::toInt<std::uint32_t>(buff + 1) != 0x464C4143u) {
|
||||
throw InvalidDataException(); // not FLAC-to-Ogg mapping header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
|
@ -35,8 +35,8 @@ void FlacToOggMappingHeader::parseHeader(OggIterator &iterator)
|
|||
// parse FLAC-to-Ogg mapping header
|
||||
m_majorVersion = static_cast<std::uint8_t>(*(buff + 0x00));
|
||||
m_minorVersion = static_cast<std::uint8_t>(*(buff + 0x01));
|
||||
m_headerCount = BE::toUInt16(buff + 0x02);
|
||||
if (BE::toUInt32(buff + 0x04) != 0x664C6143u) {
|
||||
m_headerCount = BE::toInt<std::uint16_t>(buff + 0x02);
|
||||
if (BE::toInt<std::uint32_t>(buff + 0x04) != 0x664C6143u) {
|
||||
throw InvalidDataException(); // native FLAC signature not present
|
||||
}
|
||||
|
||||
|
|
|
@ -31,8 +31,7 @@ class Diagnostics;
|
|||
*
|
||||
* For an example of such a specialization see FileElementTraits<Mp4Atom> or FileElementTraits<EbmlElement>.
|
||||
*/
|
||||
template <typename ImplementationType> class FileElementTraits {
|
||||
};
|
||||
template <typename ImplementationType> class FileElementTraits {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::GenericFileElement
|
||||
|
@ -117,13 +116,17 @@ public:
|
|||
static constexpr std::uint32_t maximumIdLengthSupported();
|
||||
static constexpr std::uint32_t maximumSizeLengthSupported();
|
||||
static constexpr std::uint8_t minimumElementSize();
|
||||
void copyHeader(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
void copyWithoutChilds(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
void copyEntirely(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
template <typename TargetStream = std::ostream>
|
||||
void copyHeader(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
template <typename TargetStream = std::ostream>
|
||||
void copyWithoutChilds(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
template <typename TargetStream = std::ostream>
|
||||
void copyEntirely(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
void makeBuffer();
|
||||
void discardBuffer();
|
||||
void copyBuffer(std::ostream &targetStream);
|
||||
void copyPreferablyFromBuffer(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
template <typename TargetStream = std::ostream> void copyBuffer(TargetStream &targetStream);
|
||||
template <typename TargetStream = std::ostream>
|
||||
void copyPreferablyFromBuffer(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
const std::unique_ptr<char[]> &buffer();
|
||||
ImplementationType *denoteFirstChild(std::uint32_t offset);
|
||||
|
||||
|
@ -140,8 +143,9 @@ protected:
|
|||
std::unique_ptr<char[]> m_buffer;
|
||||
|
||||
private:
|
||||
template <typename TargetStream = std::ostream>
|
||||
void copyInternal(
|
||||
std::ostream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
TargetStream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress);
|
||||
|
||||
ContainerType *m_container;
|
||||
bool m_parsed;
|
||||
|
@ -841,7 +845,8 @@ void GenericFileElement<ImplementationType>::validateSubsequentElementStructure(
|
|||
* \brief Writes the header information of the element to the specified \a targetStream.
|
||||
*/
|
||||
template <class ImplementationType>
|
||||
void GenericFileElement<ImplementationType>::copyHeader(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
template <typename TargetStream>
|
||||
void GenericFileElement<ImplementationType>::copyHeader(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
{
|
||||
copyInternal(targetStream, startOffset(), headerSize(), diag, progress);
|
||||
}
|
||||
|
@ -850,7 +855,8 @@ void GenericFileElement<ImplementationType>::copyHeader(std::ostream &targetStre
|
|||
* \brief Writes the element without its children to the specified \a targetStream.
|
||||
*/
|
||||
template <class ImplementationType>
|
||||
void GenericFileElement<ImplementationType>::copyWithoutChilds(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
template <typename TargetStream>
|
||||
void GenericFileElement<ImplementationType>::copyWithoutChilds(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
{
|
||||
if (std::uint32_t firstChildOffset = this->firstChildOffset()) {
|
||||
copyInternal(targetStream, startOffset(), firstChildOffset, diag, progress);
|
||||
|
@ -863,7 +869,8 @@ void GenericFileElement<ImplementationType>::copyWithoutChilds(std::ostream &tar
|
|||
* \brief Writes the entire element including all children to the specified \a targetStream.
|
||||
*/
|
||||
template <class ImplementationType>
|
||||
void GenericFileElement<ImplementationType>::copyEntirely(std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
template <typename TargetStream>
|
||||
void GenericFileElement<ImplementationType>::copyEntirely(TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
{
|
||||
copyInternal(targetStream, startOffset(), totalSize(), diag, progress);
|
||||
}
|
||||
|
@ -891,7 +898,9 @@ template <class ImplementationType> inline void GenericFileElement<Implementatio
|
|||
* \brief Copies buffered data to \a targetStream.
|
||||
* \remarks Data must have been buffered using the makeBuffer() method.
|
||||
*/
|
||||
template <class ImplementationType> inline void GenericFileElement<ImplementationType>::copyBuffer(std::ostream &targetStream)
|
||||
template <class ImplementationType>
|
||||
template <typename TargetStream>
|
||||
inline void GenericFileElement<ImplementationType>::copyBuffer(TargetStream &targetStream)
|
||||
{
|
||||
targetStream.write(m_buffer.get(), static_cast<std::streamsize>(totalSize()));
|
||||
}
|
||||
|
@ -901,8 +910,9 @@ template <class ImplementationType> inline void GenericFileElement<Implementatio
|
|||
* \remarks So this is copyBuffer() with a fallback to copyEntirely().
|
||||
*/
|
||||
template <class ImplementationType>
|
||||
template <typename TargetStream>
|
||||
inline void GenericFileElement<ImplementationType>::copyPreferablyFromBuffer(
|
||||
std::ostream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
TargetStream &targetStream, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
{
|
||||
m_buffer ? copyBuffer(targetStream) : copyEntirely(targetStream, diag, progress);
|
||||
}
|
||||
|
@ -924,8 +934,9 @@ template <class ImplementationType> inline const std::unique_ptr<char[]> &Generi
|
|||
* \sa copyEntireAtomToStream()
|
||||
*/
|
||||
template <class ImplementationType>
|
||||
template <typename TargetStream>
|
||||
void GenericFileElement<ImplementationType>::copyInternal(
|
||||
std::ostream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
TargetStream &targetStream, std::uint64_t startOffset, std::uint64_t bytesToCopy, Diagnostics &diag, AbortableProgressFeedback *progress)
|
||||
{
|
||||
// ensure the header has been parsed correctly
|
||||
try {
|
||||
|
|
|
@ -13,8 +13,7 @@ template <class implementationType> class TagField;
|
|||
*
|
||||
* A template specialization for each TagField subclass must be provided.
|
||||
*/
|
||||
template <typename ImplementationType> class TagFieldTraits {
|
||||
};
|
||||
template <typename ImplementationType> class TagFieldTraits {};
|
||||
|
||||
/*!
|
||||
* \brief The TagField class is used by FieldMapBasedTag to store the fields.
|
||||
|
|
1
global.h
1
global.h
|
@ -4,6 +4,7 @@
|
|||
#ifndef TAG_PARSER_GLOBAL
|
||||
#define TAG_PARSER_GLOBAL
|
||||
|
||||
#include "tagparser-definitions.h"
|
||||
#include <c++utilities/application/global.h>
|
||||
|
||||
#ifdef TAG_PARSER_STATIC
|
||||
|
|
|
@ -934,7 +934,7 @@ tuple<const char *, size_t, const char *> Id3v2Frame::parseSubstring(
|
|||
case TagTextEncoding::Utf16BigEndian:
|
||||
case TagTextEncoding::Utf16LittleEndian: {
|
||||
if (bufferSize >= 2) {
|
||||
switch (LE::toUInt16(buffer)) {
|
||||
switch (LE::toInt<std::uint16_t>(buffer)) {
|
||||
case 0xFEFF:
|
||||
if (encoding == TagTextEncoding::Utf16BigEndian) {
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
|
@ -1002,9 +1002,9 @@ void Id3v2Frame::parseBom(const char *buffer, std::size_t maxSize, TagTextEncodi
|
|||
switch (encoding) {
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
case TagTextEncoding::Utf16LittleEndian:
|
||||
if ((maxSize >= 2) && (BE::toUInt16(buffer) == 0xFFFE)) {
|
||||
if ((maxSize >= 2) && (BE::toInt<std::uint16_t>(buffer) == 0xFFFE)) {
|
||||
encoding = TagTextEncoding::Utf16LittleEndian;
|
||||
} else if ((maxSize >= 2) && (BE::toUInt16(buffer) == 0xFEFF)) {
|
||||
} else if ((maxSize >= 2) && (BE::toInt<std::uint16_t>(buffer) == 0xFEFF)) {
|
||||
encoding = TagTextEncoding::Utf16BigEndian;
|
||||
}
|
||||
break;
|
||||
|
@ -1172,7 +1172,7 @@ void Id3v2Frame::makeLegacyPicture(
|
|||
} else {
|
||||
imageFormat = "UND";
|
||||
}
|
||||
strncpy(++offset, imageFormat, 3);
|
||||
std::memcpy(++offset, imageFormat, 3);
|
||||
|
||||
// write picture type
|
||||
*(offset += 3) = static_cast<char>(typeInfo);
|
||||
|
|
|
@ -338,7 +338,7 @@ inline Id3v2Frame::IdentifierType Id3v2Frame::fieldIdFromString(std::string_view
|
|||
case 3:
|
||||
return CppUtilities::BE::toUInt24(idString.data());
|
||||
case 4:
|
||||
return CppUtilities::BE::toUInt32(idString.data());
|
||||
return CppUtilities::BE::toInt<std::uint32_t>(idString.data());
|
||||
default:
|
||||
throw CppUtilities::ConversionException("ID3v2 ID must be 3 or 4 chars");
|
||||
}
|
||||
|
|
|
@ -85,6 +85,10 @@ std::uint32_t convertToShortId(std::uint32_t id)
|
|||
return sRating;
|
||||
case lISRC:
|
||||
return sISRC;
|
||||
case lPublisherWebpage:
|
||||
return sPublisherWebpage;
|
||||
case lUserDefinedURL:
|
||||
return sUserDefinedURL;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
@ -155,6 +159,10 @@ std::uint32_t convertToLongId(std::uint32_t id)
|
|||
return lRating;
|
||||
case sISRC:
|
||||
return lISRC;
|
||||
case sPublisherWebpage:
|
||||
return lPublisherWebpage;
|
||||
case sUserDefinedURL:
|
||||
return lUserDefinedURL;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ enum KnownValue : std::uint32_t {
|
|||
lMood = 0x544D4F4F, /**< TMOO */
|
||||
lISRC = 0x54535243, /**< TSRC */
|
||||
lUserDefinedText = 0x54585858, /**< TXXX */
|
||||
lPublisherWebpage = 0x57505542, /**< WPUB */
|
||||
lUserDefinedURL = 0x57585858, /**< WXXX */
|
||||
|
||||
sAlbum = 0x54414c, /**< ?TAL */
|
||||
sArtist = 0x545031, /**< ?TP1 */
|
||||
|
@ -82,6 +84,8 @@ enum KnownValue : std::uint32_t {
|
|||
sCopyright = 0x544352, /**< TCR */
|
||||
sISRC = 0x545243, /**< TRC */
|
||||
sUserDefinedText = 0x545858, /**< ?TXX */
|
||||
sPublisherWebpage = 0x575042, /**< ?WPB */
|
||||
sUserDefinedURL = 0x575858, /**< ?WXX */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT std::uint32_t convertToShortId(std::uint32_t id);
|
||||
|
@ -117,6 +121,14 @@ constexpr bool isTextFrame(std::uint32_t id)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether the specified \a id is a URL frame id.
|
||||
*/
|
||||
constexpr bool isUrlFrame(std::uint32_t id)
|
||||
{
|
||||
return (id & 0xFF000000u) == 0x57000000u && (id != Id3v2FrameIds::lUserDefinedURL);
|
||||
}
|
||||
|
||||
} // namespace Id3v2FrameIds
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -141,7 +141,7 @@ Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const
|
|||
case KnownField::Comment:
|
||||
return lComment;
|
||||
case KnownField::RecordDate:
|
||||
return lRecordingTime; // (de)serializer takes to convert to/from lYear/lRecordingDates/lDate/lTime
|
||||
return lRecordingTime; // (de)serializer converts to/from lYear/lRecordingDates/lDate/lTime
|
||||
case KnownField::ReleaseDate:
|
||||
return lReleaseTime;
|
||||
case KnownField::Title:
|
||||
|
@ -205,7 +205,7 @@ Id3v2Tag::IdentifierType Id3v2Tag::internallyGetFieldId(KnownField field) const
|
|||
case KnownField::Comment:
|
||||
return sComment;
|
||||
case KnownField::RecordDate:
|
||||
return lRecordingTime; // (de)serializer takes to convert to/from sYear/sRecordingDates/sDate/sTime
|
||||
return lRecordingTime; // (de)serializer converts to/from sYear/sRecordingDates/sDate/sTime
|
||||
case KnownField::Title:
|
||||
return sTitle;
|
||||
case KnownField::Genre:
|
||||
|
@ -415,10 +415,10 @@ void Id3v2Tag::convertOldRecordDateFields(const std::string &diagContext, Diagno
|
|||
}
|
||||
|
||||
// parse values of lYear/lRecordingDates/lDate/lTime/sYear/sRecordingDates/sDate/sTime fields
|
||||
bool hasAnyValue = false;
|
||||
int year = 1, month = 1, day = 1, hour = 0, minute = 0;
|
||||
auto expr = DateTimeExpression();
|
||||
auto year = 1, month = 1, day = 1, hour = 0, minute = 0;
|
||||
if (const auto &v = value(Id3v2FrameIds::lYear)) {
|
||||
hasAnyValue = true;
|
||||
expr.parts |= DateTimeParts::Year;
|
||||
try {
|
||||
year = v.toInteger();
|
||||
} catch (const ConversionException &e) {
|
||||
|
@ -426,7 +426,7 @@ void Id3v2Tag::convertOldRecordDateFields(const std::string &diagContext, Diagno
|
|||
}
|
||||
}
|
||||
if (const auto &v = value(Id3v2FrameIds::lDate)) {
|
||||
hasAnyValue = true;
|
||||
expr.parts |= DateTimeParts::Day | DateTimeParts::Month;
|
||||
try {
|
||||
auto str = v.toString();
|
||||
if (str.size() != 4) {
|
||||
|
@ -439,7 +439,7 @@ void Id3v2Tag::convertOldRecordDateFields(const std::string &diagContext, Diagno
|
|||
}
|
||||
}
|
||||
if (const auto &v = value(Id3v2FrameIds::lTime)) {
|
||||
hasAnyValue = true;
|
||||
expr.parts |= DateTimeParts::Hour | DateTimeParts::Minute;
|
||||
try {
|
||||
auto str = v.toString();
|
||||
if (str.size() != 4) {
|
||||
|
@ -453,15 +453,18 @@ void Id3v2Tag::convertOldRecordDateFields(const std::string &diagContext, Diagno
|
|||
}
|
||||
|
||||
// set the field values as DateTime
|
||||
if (!hasAnyValue) {
|
||||
if (expr.parts == DateTimeParts::None) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setValue(Id3v2FrameIds::lRecordingTime, TagValue(DateTime::fromDateAndTime(year, month, day, hour, minute)));
|
||||
expr.value = DateTime::fromDateAndTime(year, month, day, hour, minute);
|
||||
setValue(Id3v2FrameIds::lRecordingTime, TagValue(expr));
|
||||
} catch (const ConversionException &e) {
|
||||
try {
|
||||
// try to set at least the year
|
||||
setValue(Id3v2FrameIds::lRecordingTime, TagValue(DateTime::fromDate(year)));
|
||||
expr.parts = DateTimeParts::Year;
|
||||
expr.value = DateTime::fromDate(year);
|
||||
setValue(Id3v2FrameIds::lRecordingTime, TagValue(expr));
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
argsToString(
|
||||
"Unable to parse the full date of the recording. Only the 'Year' frame could be parsed; related frames failed: ", e.what()),
|
||||
|
@ -549,7 +552,7 @@ void Id3v2Tag::parse(istream &stream, const std::uint64_t maximalSize, Diagnosti
|
|||
if (Id3v2FrameIds::isTextFrame(frame.id()) && fields().count(frame.id()) == 1) {
|
||||
diag.emplace_back(DiagLevel::Warning, "The text frame " % frame.idToString() + " exists more than once.", context);
|
||||
}
|
||||
fields().emplace(frame.id(), move(frame));
|
||||
fields().emplace(frame.id(), std::move(frame));
|
||||
} catch (const NoDataFoundException &) {
|
||||
if (frame.hasPaddingReached()) {
|
||||
m_paddingSize = startOffset + m_size - pos;
|
||||
|
@ -738,27 +741,34 @@ void Id3v2Tag::prepareRecordDataForMaking(const std::string &diagContext, Diagno
|
|||
}
|
||||
// -> convert lRecordingTime (which is supposed to be an ISO string) to a DateTime
|
||||
try {
|
||||
const auto asDateTime = recordingTime.toDateTime();
|
||||
const auto dateTimeExpr = recordingTime.toDateTimeExpression();
|
||||
const auto &asDateTime = dateTimeExpr.value;
|
||||
// -> remove any existing old fields to avoid any leftovers
|
||||
removeOldRecordDateRelatedFields();
|
||||
// -> assign old fields from parsed DateTime
|
||||
std::stringstream year, date, time;
|
||||
year << std::setfill('0') << std::setw(4) << asDateTime.year();
|
||||
setValue(Id3v2FrameIds::lYear, TagValue(year.str()));
|
||||
date << std::setfill('0') << std::setw(2) << asDateTime.day() << std::setfill('0') << std::setw(2) << asDateTime.month();
|
||||
setValue(Id3v2FrameIds::lDate, TagValue(date.str()));
|
||||
time << std::setfill('0') << std::setw(2) << asDateTime.hour() << std::setfill('0') << std::setw(2) << asDateTime.minute();
|
||||
setValue(Id3v2FrameIds::lTime, TagValue(time.str()));
|
||||
if (asDateTime.second() || asDateTime.millisecond()) {
|
||||
if (dateTimeExpr.parts & DateTimeParts::Year) {
|
||||
year << std::setfill('0') << std::setw(4) << asDateTime.year();
|
||||
setValue(Id3v2FrameIds::lYear, TagValue(year.str()));
|
||||
}
|
||||
if (dateTimeExpr.parts & (DateTimeParts::Day | DateTimeParts::Month)) {
|
||||
date << std::setfill('0') << std::setw(2) << asDateTime.day() << std::setfill('0') << std::setw(2) << asDateTime.month();
|
||||
setValue(Id3v2FrameIds::lDate, TagValue(date.str()));
|
||||
}
|
||||
if (dateTimeExpr.parts & DateTimeParts::Time) {
|
||||
time << std::setfill('0') << std::setw(2) << asDateTime.hour() << std::setfill('0') << std::setw(2) << asDateTime.minute();
|
||||
setValue(Id3v2FrameIds::lTime, TagValue(time.str()));
|
||||
}
|
||||
if (dateTimeExpr.parts & (DateTimeParts::Second | DateTimeParts::SubSecond)) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
"The recording time field (TRDA) has been truncated to full minutes when converting to corresponding fields for older ID3v2 "
|
||||
"The recording time field (TDRC) has been truncated to full minutes when converting to corresponding fields for older ID3v2 "
|
||||
"versions.",
|
||||
diagContext);
|
||||
}
|
||||
} catch (const ConversionException &e) {
|
||||
try {
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
argsToString("Unable to convert recording time field (TRDA) with the value \"", recordingTime.toString(),
|
||||
argsToString("Unable to convert recording time field (TDRC) with the value \"", recordingTime.toString(),
|
||||
"\" to corresponding fields for older ID3v2 versions: ", e.what()),
|
||||
diagContext);
|
||||
} catch (const ConversionException &) {
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace TagParser {
|
|||
|
||||
/*!
|
||||
* \class TagParser::IvfStream
|
||||
* \brief Implementation of TagParser::AbstractTrack for ADTS streams.
|
||||
* \brief Implementation of TagParser::AbstractTrack for IVF streams.
|
||||
* \sa https://wiki.multimedia.cx/index.php/IVF
|
||||
*/
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ void EbmlElement::internalParse(Diagnostics &diag)
|
|||
continue; // try again
|
||||
}
|
||||
reader().read(buf + (maximumIdLengthSupported() - m_idLength), m_idLength);
|
||||
m_id = BE::toUInt32(buf);
|
||||
m_id = BE::toInt<std::uint32_t>(buf);
|
||||
|
||||
// check whether this element is actually a sibling of one of its parents rather then a child
|
||||
// (might be the case if the parent's size is unknown and hence assumed to be the max file size)
|
||||
|
@ -119,13 +119,13 @@ void EbmlElement::internalParse(Diagnostics &diag)
|
|||
if (m_parent->firstChild() == this) {
|
||||
// ... parent
|
||||
m_parent->m_firstChild.release();
|
||||
m_parent->m_firstChild = move(m_nextSibling);
|
||||
m_parent->m_firstChild = std::move(m_nextSibling);
|
||||
} else {
|
||||
// ... previous sibling
|
||||
for (EbmlElement *sibling = m_parent->firstChild(); sibling; sibling = sibling->nextSibling()) {
|
||||
if (sibling->nextSibling() == this) {
|
||||
sibling->m_nextSibling.release();
|
||||
sibling->m_nextSibling = move(m_nextSibling);
|
||||
sibling->m_nextSibling = std::move(m_nextSibling);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ void EbmlElement::internalParse(Diagnostics &diag)
|
|||
reader().read(buf + (maximumSizeLengthSupported() - m_sizeLength), m_sizeLength);
|
||||
// xor the first byte in buffer which has been read from the file with mask
|
||||
*(buf + (maximumSizeLengthSupported() - m_sizeLength)) ^= static_cast<char>(mask);
|
||||
m_dataSize = BE::toUInt64(buf);
|
||||
m_dataSize = BE::toInt<std::uint64_t>(buf);
|
||||
// check if element is truncated
|
||||
if (totalSize() > maxTotalSize()) {
|
||||
if (m_idLength + m_sizeLength > maxTotalSize()) { // header truncated
|
||||
|
@ -242,7 +242,7 @@ std::uint64_t EbmlElement::readUInteger()
|
|||
const auto bytesToSkip = maxBytesToRead - min(dataSize(), maxBytesToRead);
|
||||
stream().seekg(static_cast<streamoff>(dataOffset()), ios_base::beg);
|
||||
stream().read(buff + bytesToSkip, static_cast<streamoff>(sizeof(buff) - bytesToSkip));
|
||||
return BE::toUInt64(buff);
|
||||
return BE::toInt<std::uint64_t>(buff);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "./matroskacontainer.h"
|
||||
#include "./matroskaid.h"
|
||||
|
||||
#include "../mediafileinfo.h"
|
||||
|
||||
#include <c++utilities/conversion/binaryconversion.h>
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <c++utilities/io/path.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
|
@ -35,8 +35,6 @@ namespace TagParser {
|
|||
* \brief Implementation of GenericContainer<MediaFileInfo, MatroskaTag, MatroskaTrack, EbmlElement>.
|
||||
*/
|
||||
|
||||
std::uint64_t MatroskaContainer::m_maxFullParseSize = 0x3200000; // FIXME v11: move to MediaFileInfo
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new container for the specified \a fileInfo at the specified \a startOffset.
|
||||
*/
|
||||
|
@ -84,13 +82,13 @@ void MatroskaContainer::reset()
|
|||
*/
|
||||
void MatroskaContainer::validateIndex(Diagnostics &diag, AbortableProgressFeedback &progress)
|
||||
{
|
||||
static const string context("validating Matroska file index (cues)");
|
||||
bool cuesElementsFound = false;
|
||||
static const auto context = std::string("validating Matroska file index (cues)");
|
||||
auto cuesElementsFound = false;
|
||||
if (m_firstElement) {
|
||||
unordered_set<EbmlElement::IdentifierType> ids;
|
||||
bool cueTimeFound = false, cueTrackPositionsFound = false;
|
||||
unique_ptr<EbmlElement> clusterElement;
|
||||
std::uint64_t pos, prevClusterSize = 0, currentOffset = 0;
|
||||
auto ids = std::unordered_set<EbmlElement::IdentifierType>();
|
||||
auto cueTimeFound = false, cueTrackPositionsFound = false;
|
||||
auto clusterElement = std::unique_ptr<EbmlElement>();
|
||||
auto pos = std::uint64_t(), prevClusterSize = std::uint64_t(), currentOffset = std::uint64_t();
|
||||
// iterate through all segments
|
||||
for (EbmlElement *segmentElement = m_firstElement->siblingById(MatroskaIds::Segment, diag); segmentElement;
|
||||
segmentElement = segmentElement->siblingById(MatroskaIds::Segment, diag)) {
|
||||
|
@ -526,31 +524,31 @@ void MatroskaContainer::internalParseHeader(Diagnostics &diag, AbortableProgress
|
|||
switch (element->id()) {
|
||||
case MatroskaIds::SegmentInfo:
|
||||
if (excludesOffset(m_segmentInfoElements, offset)) {
|
||||
m_additionalElements.emplace_back(move(element));
|
||||
m_additionalElements.emplace_back(std::move(element));
|
||||
m_segmentInfoElements.emplace_back(m_additionalElements.back().get());
|
||||
}
|
||||
break;
|
||||
case MatroskaIds::Tracks:
|
||||
if (excludesOffset(m_tracksElements, offset)) {
|
||||
m_additionalElements.emplace_back(move(element));
|
||||
m_additionalElements.emplace_back(std::move(element));
|
||||
m_tracksElements.emplace_back(m_additionalElements.back().get());
|
||||
}
|
||||
break;
|
||||
case MatroskaIds::Tags:
|
||||
if (excludesOffset(m_tagsElements, offset)) {
|
||||
m_additionalElements.emplace_back(move(element));
|
||||
m_additionalElements.emplace_back(std::move(element));
|
||||
m_tagsElements.emplace_back(m_additionalElements.back().get());
|
||||
}
|
||||
break;
|
||||
case MatroskaIds::Chapters:
|
||||
if (excludesOffset(m_chaptersElements, offset)) {
|
||||
m_additionalElements.emplace_back(move(element));
|
||||
m_additionalElements.emplace_back(std::move(element));
|
||||
m_chaptersElements.emplace_back(m_additionalElements.back().get());
|
||||
}
|
||||
break;
|
||||
case MatroskaIds::Attachments:
|
||||
if (excludesOffset(m_attachmentsElements, offset)) {
|
||||
m_additionalElements.emplace_back(move(element));
|
||||
m_additionalElements.emplace_back(std::move(element));
|
||||
m_attachmentsElements.emplace_back(m_additionalElements.back().get());
|
||||
}
|
||||
break;
|
||||
|
@ -564,7 +562,7 @@ void MatroskaContainer::internalParseHeader(Diagnostics &diag, AbortableProgress
|
|||
}
|
||||
}
|
||||
// -> stop if tracks and tags have been found or the file exceeds the max. size to fully process
|
||||
if (((!m_tracksElements.empty() && !m_tagsElements.empty()) || fileInfo().size() > m_maxFullParseSize)
|
||||
if (((!m_tracksElements.empty() && !m_tagsElements.empty()) || fileInfo().size() > fileInfo().maxFullParseSize())
|
||||
&& !m_segmentInfoElements.empty()) {
|
||||
goto finish;
|
||||
}
|
||||
|
@ -629,6 +627,12 @@ void MatroskaContainer::parseSegmentInfo(Diagnostics &diag)
|
|||
case MatroskaIds::TimeCodeScale:
|
||||
timeScale = subElement->readUInteger();
|
||||
break;
|
||||
case MatroskaIds::MuxingApp:
|
||||
muxingApplications().emplace_back(subElement->readString());
|
||||
break;
|
||||
case MatroskaIds::WrittingApp:
|
||||
writingApplications().emplace_back(subElement->readString());
|
||||
break;
|
||||
}
|
||||
subElement = subElement->nextSibling();
|
||||
}
|
||||
|
@ -822,6 +826,7 @@ struct SegmentData {
|
|||
, sizeDenotationLength(0)
|
||||
{
|
||||
}
|
||||
SegmentData(SegmentData &&) = default;
|
||||
|
||||
/// \brief whether CRC-32 checksum is present
|
||||
bool hasCrc32;
|
||||
|
@ -833,8 +838,8 @@ struct SegmentData {
|
|||
MatroskaCuePositionUpdater cuesUpdater;
|
||||
/// \brief size of the "SegmentInfo"-element
|
||||
std::uint64_t infoDataSize;
|
||||
/// \brief cluster sizes
|
||||
vector<std::uint64_t> clusterSizes;
|
||||
/// \brief cluster sizes, needed because cluster elements are not necessarily copied as-is so they're size might change
|
||||
std::vector<std::uint64_t> clusterSizes;
|
||||
/// \brief first "Cluster"-element (original file)
|
||||
EbmlElement *firstClusterElement;
|
||||
/// \brief end offset of last "Cluster"-element (original file)
|
||||
|
@ -881,15 +886,15 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
EbmlElement *level1Element, *level2Element;
|
||||
|
||||
// define variables needed for precalculation of "Tags"- and "Attachments"-element
|
||||
vector<MatroskaTagMaker> tagMaker;
|
||||
std::vector<MatroskaTagMaker> tagMaker;
|
||||
tagMaker.reserve(tags().size());
|
||||
std::uint64_t tagElementsSize = 0;
|
||||
std::uint64_t tagsSize;
|
||||
vector<MatroskaAttachmentMaker> attachmentMaker;
|
||||
std::vector<MatroskaAttachmentMaker> attachmentMaker;
|
||||
attachmentMaker.reserve(m_attachments.size());
|
||||
std::uint64_t attachedFileElementsSize = 0;
|
||||
std::uint64_t attachmentsSize;
|
||||
vector<MatroskaTrackHeaderMaker> trackHeaderMaker;
|
||||
std::vector<MatroskaTrackHeaderMaker> trackHeaderMaker;
|
||||
trackHeaderMaker.reserve(tracks().size());
|
||||
std::uint64_t trackHeaderElementsSize = 0;
|
||||
std::uint64_t trackHeaderSize;
|
||||
|
@ -898,7 +903,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
// current segment index
|
||||
unsigned int segmentIndex = 0;
|
||||
// segment specific data
|
||||
vector<SegmentData> segmentData;
|
||||
std::vector<SegmentData> segmentData;
|
||||
// offset of the segment which is currently written / offset of "Cues"-element in segment
|
||||
std::uint64_t offset;
|
||||
// current total offset (including EBML header)
|
||||
|
@ -906,7 +911,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
// current write offset (used to calculate positions)
|
||||
std::uint64_t currentPosition = 0;
|
||||
// holds the offsets of all CRC-32 elements and the length of the enclosing block
|
||||
vector<tuple<std::uint64_t, std::uint64_t>> crc32Offsets;
|
||||
std::vector<std::tuple<std::uint64_t, std::uint64_t>> crc32Offsets;
|
||||
// size length used to make size denotations
|
||||
std::uint8_t sizeLength;
|
||||
// sizes and offsets for cluster calculation
|
||||
|
@ -943,13 +948,18 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
const std::uint64_t ebmlHeaderSize = 4 + EbmlElement::calculateSizeDenotationLength(ebmlHeaderDataSize) + ebmlHeaderDataSize;
|
||||
|
||||
// calculate size of "WritingLib"-element
|
||||
constexpr std::string_view muxingAppName = APP_NAME " v" APP_VERSION;
|
||||
constexpr std::uint64_t muxingAppElementTotalSize = 2 + 1 + muxingAppName.size();
|
||||
const auto &muxingApps = const_cast<const MatroskaContainer *>(this)->muxingApplications();
|
||||
const auto muxingAppName = (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::PreserveMuxingApplication && !muxingApps.empty())
|
||||
? std::string_view(muxingApps.front())
|
||||
: std::string_view(APP_NAME " v" APP_VERSION);
|
||||
const auto muxingAppElementTotalSize = std::uint64_t(2 + 1 + muxingAppName.size());
|
||||
|
||||
// calculate size of "WritingApp"-element
|
||||
const std::uint64_t writingAppElementDataSize
|
||||
= fileInfo().writingApplication().empty() ? muxingAppName.size() : fileInfo().writingApplication().size();
|
||||
const std::uint64_t writingAppElementTotalSize = 2 + 1 + writingAppElementDataSize;
|
||||
const auto writingApps = const_cast<const MatroskaContainer *>(this)->writingApplications();
|
||||
const auto writingAppName = (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::PreserveWritingApplication && !writingApps.empty())
|
||||
? std::string_view(writingApps.front())
|
||||
: std::string_view(fileInfo().writingApplication().empty() ? muxingAppName : std::string_view(fileInfo().writingApplication()));
|
||||
const auto writingAppElementTotalSize = std::uint64_t(2 + 1 + writingAppName.size());
|
||||
|
||||
try {
|
||||
// calculate size of "Tags"-element
|
||||
|
@ -1218,7 +1228,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
progress.updateStep("Calculating cluster offsets ...");
|
||||
}
|
||||
|
||||
// decided whether it is necessary to rewrite the entire file (if not already rewriting)
|
||||
// decide whether it is necessary to rewrite the entire file (if not already rewriting)
|
||||
if (!rewriteRequired) {
|
||||
// find first "Cluster"-element
|
||||
if ((level1Element = segment.firstClusterElement)) {
|
||||
|
@ -1356,7 +1366,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
goto calculateSegmentData;
|
||||
}
|
||||
} else {
|
||||
// if rewrite is required, pretend writing the remaining elements to compute total segment size
|
||||
// if rewrite is required, pretend writing the remaining elements to compute total segment size and cluster sizes
|
||||
|
||||
// pretend writing "Void"-element (only if there is at least one "Cluster"-element in the segment)
|
||||
if (!segmentIndex && rewriteRequired && (level1Element = level0Element->childById(MatroskaIds::Cluster, diag))) {
|
||||
|
@ -1638,8 +1648,7 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
}
|
||||
// -> write "MuxingApp"- and "WritingApp"-element
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::MuxingApp, muxingAppName);
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::WrittingApp,
|
||||
fileInfo().writingApplication().empty() ? muxingAppName : fileInfo().writingApplication());
|
||||
EbmlElement::makeSimpleElement(outputStream, MatroskaIds::WrittingApp, writingAppName);
|
||||
}
|
||||
|
||||
// write "Tracks"-element
|
||||
|
@ -1837,10 +1846,12 @@ void MatroskaContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFee
|
|||
// -> close stream before truncating
|
||||
outputStream.close();
|
||||
// -> truncate file
|
||||
if (truncate(fileInfo().path().c_str(), static_cast<iostream::off_type>(newSize)) == 0) {
|
||||
auto ec = std::error_code();
|
||||
std::filesystem::resize_file(makeNativePath(fileInfo().path()), newSize, ec);
|
||||
if (!ec) {
|
||||
fileInfo().reportSizeChanged(newSize);
|
||||
} else {
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
|
||||
}
|
||||
// -> reopen the stream again
|
||||
outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
|
||||
|
|
|
@ -31,8 +31,6 @@ public:
|
|||
std::uint64_t maxSizeLength() const;
|
||||
const std::vector<std::unique_ptr<MatroskaSeekInfo>> &seekInfos() const;
|
||||
|
||||
static std::uint64_t maxFullParseSize();
|
||||
void setMaxFullParseSize(std::uint64_t maxFullParseSize);
|
||||
const std::vector<std::unique_ptr<MatroskaEditionEntry>> &editionEntires() const;
|
||||
MatroskaChapter *chapter(std::size_t index) override;
|
||||
std::size_t chapterCount() const override;
|
||||
|
@ -72,7 +70,6 @@ private:
|
|||
std::vector<std::unique_ptr<MatroskaEditionEntry>> m_editionEntries;
|
||||
std::vector<std::unique_ptr<MatroskaAttachment>> m_attachments;
|
||||
std::size_t m_segmentCount;
|
||||
static std::uint64_t m_maxFullParseSize;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -99,33 +96,6 @@ inline const std::vector<std::unique_ptr<MatroskaSeekInfo>> &MatroskaContainer::
|
|||
return m_seekInfos;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the maximal file size for a "full parse" in byte.
|
||||
*
|
||||
* The "Tags" element (which holds the tag information) is commonly at the end of a Matroska file. Hence the
|
||||
* parser needs to walk through the entire file to find the tag information if no "SeekHead" element is present
|
||||
* which might causes long loading times. To avoid this a maximal file size for a "full parse" can be specified.
|
||||
* The disadvantage is that the parser relies on the presence of a SeekHead element on larger files to retrieve
|
||||
* tag information.
|
||||
*
|
||||
* The default value is 50 MiB.
|
||||
*
|
||||
* \sa setMaxFullParseSize()
|
||||
*/
|
||||
inline std::uint64_t MatroskaContainer::maxFullParseSize()
|
||||
{
|
||||
return m_maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the maximal file size for a "full parse" in byte.
|
||||
* \sa maxFullParseSize()
|
||||
*/
|
||||
inline void MatroskaContainer::setMaxFullParseSize(std::uint64_t maxFullParseSize)
|
||||
{
|
||||
m_maxFullParseSize = maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the edition entries.
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "./matroskacues.h"
|
||||
#include "./matroskacontainer.h"
|
||||
|
||||
#include "../mediafileinfo.h"
|
||||
|
||||
#include <c++utilities/conversion/binaryconversion.h>
|
||||
|
||||
using namespace std;
|
||||
|
@ -76,7 +78,7 @@ void MatroskaCuePositionUpdater::parse(EbmlElement *cuesElement, Diagnostics &di
|
|||
cuePointElementSize += cuePointChild->totalSize();
|
||||
break;
|
||||
case MatroskaIds::CueTrackPositions:
|
||||
cueTrackPositionsElementSize = 0;
|
||||
cueTrackPositionsElementSize = relPos = 0;
|
||||
cueRelativePositionElement = cueClusterPositionElement = nullptr;
|
||||
for (EbmlElement *cueTrackPositionsChild = cuePointChild->firstChild(); cueTrackPositionsChild;
|
||||
cueTrackPositionsChild = cueTrackPositionsChild->nextSibling()) {
|
||||
|
|
|
@ -122,7 +122,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "track number";
|
||||
case TrackUID:
|
||||
return "unique track id";
|
||||
case TrackType:
|
||||
case TrackEntryIds::TrackType:
|
||||
return "track type";
|
||||
case TrackAudio:
|
||||
return "audio track";
|
||||
|
@ -192,7 +192,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "video display width";
|
||||
case DisplayHeight:
|
||||
return "video display height";
|
||||
case DisplayUnit:
|
||||
case TrackVideoIds::DisplayUnit:
|
||||
return "video display unit";
|
||||
case PixelWidth:
|
||||
return "video pixel width";
|
||||
|
@ -208,9 +208,9 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "video pixel crop right";
|
||||
case FlagInterlaced:
|
||||
return "video flag interlaced";
|
||||
case StereoMode:
|
||||
case TrackVideoIds::StereoMode:
|
||||
return "video stereo mode";
|
||||
case AspectRatioType:
|
||||
case TrackVideoIds::AspectRatioType:
|
||||
return "video aspect ratio type";
|
||||
case ColorSpace:
|
||||
return "video color space";
|
||||
|
@ -276,7 +276,7 @@ std::string_view matroskaIdName(std::uint32_t matroskaId)
|
|||
return "content encryption signature hash algorithmus";
|
||||
|
||||
// IDs in the Tags master
|
||||
case Tag:
|
||||
case TagsIds::Tag:
|
||||
return "tag";
|
||||
|
||||
// IDs in the Tag master
|
||||
|
@ -569,7 +569,7 @@ MatroskaElementLevel matroskaIdLevel(std::uint32_t matroskaId)
|
|||
case CuePoint:
|
||||
case AttachedFile:
|
||||
case EditionEntry:
|
||||
case Tag:
|
||||
case TagsIds::Tag:
|
||||
return MatroskaElementLevel::Level2;
|
||||
case SeekID:
|
||||
case SeekPosition:
|
||||
|
@ -588,7 +588,7 @@ MatroskaElementLevel matroskaIdLevel(std::uint32_t matroskaId)
|
|||
case Slices:
|
||||
case TrackNumber:
|
||||
case TrackUID:
|
||||
case TrackType:
|
||||
case TrackEntryIds::TrackType:
|
||||
case TrackFlagEnabled:
|
||||
case TrackFlagDefault:
|
||||
case TrackFlagForced:
|
||||
|
|
|
@ -43,7 +43,7 @@ enum SeekIds { SeekID = 0x53AB, SeekPosition = 0x53AC };
|
|||
enum SegmentInfoIds {
|
||||
TimeCodeScale = 0x2AD7B1,
|
||||
Duration = 0x4489,
|
||||
WrittingApp = 0x5741,
|
||||
WrittingApp = 0x5741, // TODOv13: change to WritingApp
|
||||
MuxingApp = 0x4D80,
|
||||
DateUTC = 0x4461,
|
||||
SegmentUID = 0x73A4,
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
#include "./ebmlelement.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace TagParser {
|
||||
|
||||
|
|
|
@ -74,10 +74,10 @@ void MatroskaTagField::reparse(EbmlElement &simpleTagElement, Diagnostics &diag,
|
|||
child->stream().read(buffer.get(), static_cast<streamoff>(child->dataSize()));
|
||||
switch (child->id()) {
|
||||
case MatroskaIds::TagString:
|
||||
value().assignData(move(buffer), child->dataSize(), TagDataType::Text, TagTextEncoding::Utf8);
|
||||
value().assignData(std::move(buffer), child->dataSize(), TagDataType::Text, TagTextEncoding::Utf8);
|
||||
break;
|
||||
case MatroskaIds::TagBinary:
|
||||
value().assignData(move(buffer), child->dataSize(), TagDataType::Undefined);
|
||||
value().assignData(std::move(buffer), child->dataSize(), TagDataType::Undefined);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -37,11 +37,12 @@
|
|||
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <c++utilities/io/path.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
|
@ -59,6 +60,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The MediaFileInfoPrivate struct contains private fields of the MediaFileInfo class.
|
||||
struct MediaFileInfoPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::MediaFileInfo
|
||||
* \brief The MediaFileInfo class allows to read and write tag information providing
|
||||
|
@ -80,6 +84,7 @@ MediaFileInfo::MediaFileInfo(std::string &&path)
|
|||
, m_containerFormat(ContainerFormat::Unknown)
|
||||
, m_containerOffset(0)
|
||||
, m_paddingSize(0)
|
||||
, m_effectiveSize(0)
|
||||
, m_fileStructureFlags(MediaFileStructureFlags::None)
|
||||
, m_tracksParsingStatus(ParsingStatus::NotParsedYet)
|
||||
, m_tagsParsingStatus(ParsingStatus::NotParsedYet)
|
||||
|
@ -92,6 +97,7 @@ MediaFileInfo::MediaFileInfo(std::string &&path)
|
|||
, m_indexPosition(ElementPosition::BeforeData)
|
||||
, m_fileHandlingFlags(MediaFileHandlingFlags::ForceRewrite | MediaFileHandlingFlags::ForceTagPosition | MediaFileHandlingFlags::ForceIndexPosition
|
||||
| MediaFileHandlingFlags::NormalizeKnownTagFieldIds | MediaFileHandlingFlags::PreserveRawTimingValues)
|
||||
, m_maxFullParseSize(0x3200000)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -148,6 +154,7 @@ void MediaFileInfo::parseContainerFormat(Diagnostics &diag, AbortableProgressFee
|
|||
m_paddingSize = 0;
|
||||
m_containerOffset = 0;
|
||||
std::size_t bytesSkippedBeforeContainer = 0;
|
||||
std::streamoff id3v2Size = 0;
|
||||
|
||||
// read signatrue
|
||||
char buff[16];
|
||||
|
@ -178,6 +185,7 @@ startParsingSignature:
|
|||
if ((bytesSkippedBeforeContainer += bytesSkipped) >= 0x800u) {
|
||||
m_containerParsingStatus = ParsingStatus::NotSupported;
|
||||
m_containerFormat = ContainerFormat::Unknown;
|
||||
m_containerOffset = id3v2Size;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -187,7 +195,7 @@ startParsingSignature:
|
|||
|
||||
// parse signature
|
||||
switch ((m_containerFormat = parseSignature(buff, sizeof(buff)))) {
|
||||
case ContainerFormat::Id2v2Tag:
|
||||
case ContainerFormat::Id3v2Tag:
|
||||
// save position of ID3v2 tag
|
||||
m_actualId3v2TagOffsets.push_back(m_containerOffset);
|
||||
if (m_actualId3v2TagOffsets.size() == 2) {
|
||||
|
@ -199,11 +207,12 @@ startParsingSignature:
|
|||
stream().read(buff, 5);
|
||||
|
||||
// set the container offset to skip ID3v2 header
|
||||
m_containerOffset += toNormalInt(BE::toUInt32(buff + 1)) + 10;
|
||||
m_containerOffset += toNormalInt(BE::toInt<std::uint32_t>(buff + 1)) + 10;
|
||||
if ((*buff) & 0x10) {
|
||||
// footer present
|
||||
m_containerOffset += 10;
|
||||
}
|
||||
id3v2Size = m_containerOffset;
|
||||
|
||||
// continue reading signature
|
||||
goto startParsingSignature;
|
||||
|
@ -242,7 +251,7 @@ startParsingSignature:
|
|||
} catch (const Failure &) {
|
||||
m_containerParsingStatus = ParsingStatus::CriticalFailure;
|
||||
}
|
||||
m_container = move(container);
|
||||
m_container = std::move(container);
|
||||
break;
|
||||
}
|
||||
case ContainerFormat::Ogg:
|
||||
|
@ -251,6 +260,23 @@ startParsingSignature:
|
|||
static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(isForcingFullParse());
|
||||
break;
|
||||
case ContainerFormat::Unknown:
|
||||
case ContainerFormat::ApeTag:
|
||||
// skip APE tag if the specified size makes sense at all
|
||||
if (m_containerFormat == ContainerFormat::ApeTag) {
|
||||
if (const auto apeEnd = m_containerOffset + 32 + LE::toUInt32(buff + 12); apeEnd <= static_cast<std::streamoff>(size())) {
|
||||
// take record of APE tag
|
||||
diag.emplace_back(DiagLevel::Critical,
|
||||
argsToString("Found an APE tag at the beginning of the file at offset ", m_containerOffset,
|
||||
". This tag format is not supported and the tag will therefore be ignored. It will NOT be preserved when saving as "
|
||||
"placing an APE tag at the beginning of a file is strongly unrecommended."),
|
||||
context);
|
||||
// continue reading signature
|
||||
m_containerOffset = apeEnd;
|
||||
goto startParsingSignature;
|
||||
}
|
||||
m_containerFormat = ContainerFormat::Unknown;
|
||||
}
|
||||
|
||||
// check for magic numbers at odd offsets
|
||||
// -> check for tar (magic number at offset 0x101)
|
||||
if (size() > 0x107) {
|
||||
|
@ -331,6 +357,13 @@ void MediaFileInfo::parseTracks(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
default:
|
||||
throw NotImplementedException();
|
||||
}
|
||||
if (m_containerFormat != ContainerFormat::Flac) {
|
||||
// ensure the effective size has been determined
|
||||
// note: This is not required for FLAC and should also be avoided as parseTags() will invoke
|
||||
// parseTracks() when dealing with FLAC files.
|
||||
parseTags(diag, progress);
|
||||
m_singleTrack->setSize(m_effectiveSize);
|
||||
}
|
||||
m_singleTrack->parseHeader(diag, progress);
|
||||
|
||||
// take padding for some "single-track" formats into account
|
||||
|
@ -375,12 +408,14 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
static const string context("parsing tag");
|
||||
|
||||
// check for ID3v1 tag
|
||||
if (size() >= 128) {
|
||||
auto effectiveSize = static_cast<std::streamoff>(size());
|
||||
if (effectiveSize >= 128) {
|
||||
m_id3v1Tag = make_unique<Id3v1Tag>();
|
||||
try {
|
||||
stream().seekg(-128, ios_base::end);
|
||||
stream().seekg(effectiveSize - 128, std::ios_base::beg);
|
||||
m_id3v1Tag->parse(stream(), diag);
|
||||
m_fileStructureFlags += MediaFileStructureFlags::ActualExistingId3v1Tag;
|
||||
effectiveSize -= 128;
|
||||
} catch (const NoDataFoundException &) {
|
||||
m_id3v1Tag.reset();
|
||||
} catch (const OperationAbortedException &) {
|
||||
|
@ -392,6 +427,31 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
}
|
||||
}
|
||||
|
||||
// check for APE tag at the end of the file (APE tags a the beginning are already covered when parsing the container format)
|
||||
if (constexpr auto apeHeaderSize = 32; effectiveSize >= apeHeaderSize) {
|
||||
const auto footerOffset = effectiveSize - apeHeaderSize;
|
||||
char buffer[apeHeaderSize];
|
||||
stream().seekg(footerOffset, std::ios_base::beg);
|
||||
stream().read(buffer, sizeof(buffer));
|
||||
if (BE::toInt<std::uint64_t>(buffer) == 0x4150455441474558ul /* APETAGEX */) {
|
||||
// take record of APE tag
|
||||
const auto tagSize = static_cast<std::streamoff>(LE::toInt<std::uint32_t>(buffer + 12));
|
||||
const auto flags = LE::toInt<std::uint32_t>(buffer + 20);
|
||||
// subtract tag size (footer size and contents) from effective size
|
||||
if (tagSize <= effectiveSize) {
|
||||
effectiveSize -= tagSize;
|
||||
}
|
||||
// subtract header size (not included in tag size) from effective size if flags indicate presence of header
|
||||
if ((flags & 0x80000000u) && (apeHeaderSize <= effectiveSize)) {
|
||||
effectiveSize -= apeHeaderSize;
|
||||
}
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Found an APE tag at the end of the file at offset ", (footerOffset - tagSize),
|
||||
". This tag format is not supported and the tag will therefore be ignored. It will be preserved when saving as-is."),
|
||||
context);
|
||||
}
|
||||
}
|
||||
|
||||
// check for ID3v2 tags: the offsets of the ID3v2 tags have already been parsed when parsing the container format
|
||||
m_id3v2Tags.clear();
|
||||
for (const auto offset : m_actualId3v2TagOffsets) {
|
||||
|
@ -412,6 +472,9 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
m_id3v2Tags.emplace_back(id3v2Tag.release());
|
||||
}
|
||||
|
||||
// compute effective size
|
||||
m_effectiveSize = static_cast<std::uint64_t>(effectiveSize - m_containerOffset);
|
||||
|
||||
// check for tags in tracks (FLAC only) or via container object
|
||||
try {
|
||||
if (m_containerFormat == ContainerFormat::Flac) {
|
||||
|
@ -422,7 +485,7 @@ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &prog
|
|||
return;
|
||||
} else if (m_container) {
|
||||
m_container->parseTags(diag, progress);
|
||||
} else {
|
||||
} else if (m_containerFormat != ContainerFormat::MpegAudioFrames) {
|
||||
throw NotImplementedException();
|
||||
}
|
||||
|
||||
|
@ -598,6 +661,7 @@ bool MediaFileInfo::createAppropriateTags(const TagCreationSettings &settings)
|
|||
if (!hasAnyTag() && !(flags & TagCreationFlags::TreatUnknownFilesAsMp3Files)) {
|
||||
switch (containerFormat()) {
|
||||
case ContainerFormat::Adts:
|
||||
case ContainerFormat::Aiff:
|
||||
case ContainerFormat::MpegAudioFrames:
|
||||
case ContainerFormat::WavPack:
|
||||
break;
|
||||
|
@ -958,7 +1022,7 @@ string MediaFileInfo::technicalSummary() const
|
|||
for (size_t i = 0; i != trackCount; ++i) {
|
||||
const string description(m_container->track(i)->description());
|
||||
if (!description.empty()) {
|
||||
parts.emplace_back(move(description));
|
||||
parts.emplace_back(std::move(description));
|
||||
}
|
||||
}
|
||||
return joinStrings(parts, " / ");
|
||||
|
@ -1359,7 +1423,7 @@ void MediaFileInfo::mergeId3v2Tags()
|
|||
for (auto i = isecond; i != end; ++i) {
|
||||
first.insertFields(**i, false);
|
||||
}
|
||||
m_id3v2Tags.erase(isecond, end - 1);
|
||||
m_id3v2Tags.erase(isecond, end);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -1595,10 +1659,12 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
}
|
||||
progress.updateStep("Removing ID3v1 tag ...");
|
||||
stream().close();
|
||||
if (truncate(BasicFileInfo::pathForOpen(path()).data(), static_cast<std::streamoff>(size() - 128)) == 0) {
|
||||
auto ec = std::error_code();
|
||||
std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), size() - 128, ec);
|
||||
if (!ec) {
|
||||
reportSizeChanged(size() - 128);
|
||||
} else {
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate file to remove ID3v1 tag.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate file to remove ID3v1 tag: " + ec.message(), context);
|
||||
throw std::ios_base::failure("Unable to truncate file to remove ID3v1 tag.");
|
||||
}
|
||||
return;
|
||||
|
@ -1636,9 +1702,9 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
progress.updateStep(flacStream ? "Updating FLAC tags ..." : "Updating ID3v2 tags ...");
|
||||
|
||||
// prepare ID3v2 tags
|
||||
vector<Id3v2TagMaker> makers;
|
||||
auto makers = std::vector<Id3v2TagMaker>();
|
||||
makers.reserve(m_id3v2Tags.size());
|
||||
std::uint64_t tagsSize = 0;
|
||||
auto tagsSize = std::uint64_t();
|
||||
for (auto &tag : m_id3v2Tags) {
|
||||
try {
|
||||
makers.emplace_back(tag->prepareMaking(diag));
|
||||
|
@ -1648,10 +1714,10 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
}
|
||||
|
||||
// determine stream offset and make track/format specific metadata
|
||||
std::uint32_t streamOffset; // where the actual stream starts
|
||||
stringstream flacMetaData(ios_base::in | ios_base::out | ios_base::binary);
|
||||
flacMetaData.exceptions(ios_base::badbit | ios_base::failbit);
|
||||
std::streamoff startOfLastMetaDataBlock;
|
||||
auto streamOffset = std::uint32_t(); // where the actual stream starts
|
||||
auto flacMetaData = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
|
||||
flacMetaData.exceptions(std::ios_base::badbit | std::ios_base::failbit);
|
||||
auto startOfLastMetaDataBlock = std::streamoff();
|
||||
if (flacStream) {
|
||||
// if it is a raw FLAC stream, make FLAC metadata
|
||||
startOfLastMetaDataBlock = flacStream->makeHeader(flacMetaData, diag);
|
||||
|
@ -1663,8 +1729,8 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
}
|
||||
|
||||
// check whether rewrite is required
|
||||
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
|
||||
size_t padding = 0;
|
||||
auto rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
|
||||
auto padding = std::size_t();
|
||||
if (!rewriteRequired) {
|
||||
// rewriting is not forced and new tag is not too big for available space
|
||||
// -> calculate new padding
|
||||
|
@ -1800,12 +1866,12 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
progress.updateStep("Writing MPEG audio frames ...");
|
||||
break;
|
||||
default:
|
||||
progress.updateStep("Writing frames ...");
|
||||
progress.updateStep("Writing data ...");
|
||||
}
|
||||
backupStream.seekg(static_cast<streamoff>(streamOffset));
|
||||
CopyHelper<0x4000> copyHelper;
|
||||
copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, bind(&AbortableProgressFeedback::isAborted, ref(progress)),
|
||||
bind(&AbortableProgressFeedback::updateStepPercentage, ref(progress), _1));
|
||||
copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, std::bind(&AbortableProgressFeedback::isAborted, std::ref(progress)),
|
||||
std::bind(&AbortableProgressFeedback::updateStepPercentage, std::ref(progress), _1));
|
||||
} else {
|
||||
// just skip actual stream data
|
||||
outputStream.seekp(static_cast<std::streamoff>(mediaDataSize), ios_base::cur);
|
||||
|
@ -1840,10 +1906,12 @@ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &pr
|
|||
// -> prevent deferring final write operations
|
||||
outputStream.close();
|
||||
// -> truncate file
|
||||
if (truncate(BasicFileInfo::pathForOpen(path()).data(), static_cast<streamoff>(newSize)) == 0) {
|
||||
auto ec = std::error_code();
|
||||
std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), newSize, ec);
|
||||
if (!ec) {
|
||||
reportSizeChanged(newSize);
|
||||
} else {
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
|
||||
}
|
||||
} else {
|
||||
// file is longer after the modification
|
||||
|
|
|
@ -64,6 +64,11 @@ enum class MediaFileHandlingFlags : std::uint64_t {
|
|||
ForceIndexPosition = (1 << 3), /**< enforces the index position when applying changes, see remarks of MediaFileInfo::setIndexPosition() */
|
||||
NormalizeKnownTagFieldIds = (1 << 4), /**< normalizes known tag field IDs when parsing to match the tag specification's recommendations */
|
||||
PreserveRawTimingValues = (1 << 8), /**< preverves raw timing values (so far only used when making MP4 tracks) */
|
||||
PreserveMuxingApplication = (1 << 9), /**< preverves the muxing application (so far only used when making Matroska container) */
|
||||
PreserveWritingApplication = (1 << 10), /**< preverves the writing application (so far only used when making Matroska container) */
|
||||
ConvertTotalFields = (1 << 11), /**< ensures fields usually holding PositionInSet values such as KnownField::TrackPosition are actually
|
||||
stored as such (and *not* as two separate fields for the position and total values); currently only relevant for Vorbis Comments
|
||||
\sa VorbisCommentFlags::ConvertTotalFields */
|
||||
};
|
||||
|
||||
} // namespace TagParser
|
||||
|
@ -73,6 +78,8 @@ CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(TagParser, TagParser::MediaFileHandlingFlags)
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
struct MediaFileInfoPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT MediaFileInfo : public BasicFileInfo {
|
||||
public:
|
||||
// constructor, destructor
|
||||
|
@ -103,9 +110,10 @@ public:
|
|||
std::string_view mimeType() const;
|
||||
std::uint64_t containerOffset() const;
|
||||
std::uint64_t paddingSize() const;
|
||||
std::uint64_t effectiveSize() const;
|
||||
AbstractContainer *container() const;
|
||||
ParsingStatus containerParsingStatus() const;
|
||||
// ... the capters
|
||||
// ... the chapters
|
||||
ParsingStatus chaptersParsingStatus() const;
|
||||
std::vector<AbstractChapter *> chapters() const;
|
||||
bool areChaptersSupported() const;
|
||||
|
@ -184,6 +192,8 @@ public:
|
|||
void setIndexPosition(ElementPosition indexPosition);
|
||||
bool forceIndexPosition() const;
|
||||
void setForceIndexPosition(bool forceTagPosition);
|
||||
std::uint64_t maxFullParseSize() const;
|
||||
void setMaxFullParseSize(std::uint64_t maxFullParseSize);
|
||||
|
||||
protected:
|
||||
void invalidated() override;
|
||||
|
@ -199,6 +209,7 @@ private:
|
|||
ContainerFormat m_containerFormat;
|
||||
std::streamoff m_containerOffset;
|
||||
std::uint64_t m_paddingSize;
|
||||
std::uint64_t m_effectiveSize;
|
||||
std::vector<std::streamoff> m_actualId3v2TagOffsets;
|
||||
std::unique_ptr<AbstractContainer> m_container;
|
||||
MediaFileStructureFlags m_fileStructureFlags;
|
||||
|
@ -226,6 +237,8 @@ private:
|
|||
ElementPosition m_tagPosition;
|
||||
ElementPosition m_indexPosition;
|
||||
MediaFileHandlingFlags m_fileHandlingFlags;
|
||||
std::uint64_t m_maxFullParseSize;
|
||||
std::unique_ptr<MediaFileInfoPrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -293,6 +306,15 @@ inline std::uint64_t MediaFileInfo::paddingSize() const
|
|||
return m_paddingSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the "effective size" of the file if know; otherwise returns 0.
|
||||
* \remarks This is the size of the file minus tags at the beginning and the end.
|
||||
*/
|
||||
inline std::uint64_t MediaFileInfo::effectiveSize() const
|
||||
{
|
||||
return m_effectiveSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether tag information has been parsed yet.
|
||||
*/
|
||||
|
@ -454,7 +476,9 @@ inline const std::string &MediaFileInfo::writingApplication() const
|
|||
|
||||
/*!
|
||||
* \brief Sets the writing application as container-level meta-data. Put the name of your application here.
|
||||
* \remarks Might not be used (depends on the format).
|
||||
* \remarks
|
||||
* - Currently only used when making Matroska files.
|
||||
* - The assigned value is ignored when MediaFileHandlingFlags::PreserveWritingApplication is set.
|
||||
*/
|
||||
inline void MediaFileInfo::setWritingApplication(std::string_view writingApplication)
|
||||
{
|
||||
|
@ -692,6 +716,33 @@ inline void MediaFileInfo::setForceIndexPosition(bool forceIndexPosition)
|
|||
CppUtilities::modFlagEnum(m_fileHandlingFlags, MediaFileHandlingFlags::ForceIndexPosition, forceIndexPosition);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the maximal file size for a "full parse" in byte.
|
||||
* \remarks
|
||||
* So far this is Matroska-specific: The "Tags" element (which holds the tag information) is commonly at the end
|
||||
* of a Matroska file. Hence the parser needs to walk through the entire file to find the tag information if no
|
||||
* "SeekHead" element is present which might causes long loading times. To avoid this a maximal file size for a
|
||||
* "full parse" can be specified. The disadvantage is that the parser relies on the presence of a SeekHead element
|
||||
* on larger files to retrieve tag information.
|
||||
*
|
||||
* The default value is 50 MiB.
|
||||
*
|
||||
* \sa setMaxFullParseSize()
|
||||
*/
|
||||
inline std::uint64_t MediaFileInfo::maxFullParseSize() const
|
||||
{
|
||||
return m_maxFullParseSize;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the maximal file size for a "full parse" in byte.
|
||||
* \sa maxFullParseSize()
|
||||
*/
|
||||
inline void MediaFileInfo::setMaxFullParseSize(std::uint64_t maxFullParseSize)
|
||||
{
|
||||
m_maxFullParseSize = maxFullParseSize;
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
||||
#endif // TAG_PARSER_MEDIAINFO_H
|
||||
|
|
|
@ -135,6 +135,8 @@ std::string_view MediaFormat::name() const
|
|||
return "3GPP2 Compact Multimedia Format (CMF)";
|
||||
case GeneralMediaFormat::Hevc:
|
||||
return "High Efficiency Video Coding";
|
||||
case GeneralMediaFormat::Vcc:
|
||||
return "Versatile Video Coding";
|
||||
case GeneralMediaFormat::ImaadpcmAcm:
|
||||
return "IMAADPCM ACM";
|
||||
case GeneralMediaFormat::ImageSubtitle:
|
||||
|
@ -564,6 +566,8 @@ std::string_view MediaFormat::abbreviation() const
|
|||
return "3GPP2 CMF";
|
||||
case GeneralMediaFormat::Hevc:
|
||||
return "H.265";
|
||||
case GeneralMediaFormat::Vcc:
|
||||
return "H.266";
|
||||
case GeneralMediaFormat::ImaadpcmAcm:
|
||||
return "IMAADPCM ACM";
|
||||
case GeneralMediaFormat::ImageSubtitle:
|
||||
|
@ -873,6 +877,8 @@ std::string_view MediaFormat::shortAbbreviation() const
|
|||
return "3GPP2-CMF";
|
||||
case GeneralMediaFormat::Hevc:
|
||||
return "H.265";
|
||||
case GeneralMediaFormat::Vcc:
|
||||
return "H.266";
|
||||
case GeneralMediaFormat::ImaadpcmAcm:
|
||||
return "IMAADPCM-ACM";
|
||||
case GeneralMediaFormat::ImageSubtitle:
|
||||
|
|
|
@ -99,6 +99,7 @@ enum class GeneralMediaFormat : unsigned int {
|
|||
WavPack, /**< WavPack */
|
||||
WindowsMediaAudio, /**< Windows Media Audio */
|
||||
WindowsMediaVideo, /**< Windows Media Video */
|
||||
Vcc, /**< H.266/Versatile Video Coding */
|
||||
};
|
||||
|
||||
/*!
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
|
||||
|
||||
#include "./mp4container.h"
|
||||
#include "./mp4ids.h"
|
||||
|
||||
|
@ -11,9 +9,9 @@
|
|||
#include <c++utilities/io/binaryreader.h>
|
||||
#include <c++utilities/io/binarywriter.h>
|
||||
#include <c++utilities/io/copy.h>
|
||||
#include <c++utilities/io/path.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <tuple>
|
||||
|
@ -243,7 +241,7 @@ void Mp4Container::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
// -> whether media data is written chunk by chunk (need to write chunk by chunk if tracks have been altered)
|
||||
const bool writeChunkByChunk = m_tracksAltered;
|
||||
// -> whether rewrite is required (always required when forced to rewrite or when tracks have been altered)
|
||||
bool rewriteRequired = fileInfo().isForcingRewrite() || writeChunkByChunk;
|
||||
bool rewriteRequired = fileInfo().isForcingRewrite() || writeChunkByChunk || !fileInfo().saveFilePath().empty();
|
||||
// -> use the preferred tag position/index position (force one wins, if both are force tag pos wins; might be changed later if none is forced)
|
||||
ElementPosition initialNewTagPos
|
||||
= fileInfo().forceTagPosition() || !fileInfo().forceIndexPosition() ? fileInfo().tagPosition() : fileInfo().indexPosition();
|
||||
|
@ -269,7 +267,7 @@ void Mp4Container::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
|
||||
// find relevant atoms in original file
|
||||
Mp4Atom *fileTypeAtom, *progressiveDownloadInfoAtom, *movieAtom, *firstMediaDataAtom, *firstMovieFragmentAtom /*, *userDataAtom*/;
|
||||
Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten;
|
||||
Mp4Atom *level0Atom, *level1Atom, *level2Atom, *lastAtomToBeWritten = nullptr;
|
||||
try {
|
||||
// file type atom (mandatory)
|
||||
if ((fileTypeAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::FileType, diag))) {
|
||||
|
@ -277,7 +275,7 @@ void Mp4Container::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
fileTypeAtom->makeBuffer();
|
||||
} else {
|
||||
// throw error if missing
|
||||
diag.emplace_back(DiagLevel::Critical, "Mandatory \"ftyp\"-atom not found.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Mandatory \"ftyp\"-atom not found in the source file.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
|
@ -290,7 +288,7 @@ void Mp4Container::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
// movie atom (mandatory)
|
||||
if (!(movieAtom = firstElement()->siblingByIdIncludingThis(Mp4AtomIds::Movie, diag))) {
|
||||
// throw error if missing
|
||||
diag.emplace_back(DiagLevel::Critical, "Mandatory \"moov\"-atom not in the source file found.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Mandatory \"moov\"-atom not found in the source file.", context);
|
||||
throw InvalidDataException();
|
||||
}
|
||||
|
||||
|
@ -749,7 +747,7 @@ calculatePadding:
|
|||
|
||||
// increase total chunk count and size
|
||||
totalChunkCount += track->chunkCount();
|
||||
totalMediaDataSize += accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), 0ul);
|
||||
totalMediaDataSize += std::accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), static_cast<std::uint64_t>(0u));
|
||||
}
|
||||
|
||||
// write media data chunk-by-chunk
|
||||
|
@ -838,10 +836,12 @@ calculatePadding:
|
|||
// -> close stream before truncating
|
||||
outputStream.close();
|
||||
// -> truncate file
|
||||
if (truncate(BasicFileInfo::pathForOpen(fileInfo().path()).data(), static_cast<iostream::off_type>(newSize)) == 0) {
|
||||
auto ec = std::error_code();
|
||||
std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(fileInfo().path())), newSize, ec);
|
||||
if (!ec) {
|
||||
fileInfo().reportSizeChanged(newSize);
|
||||
} else {
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file.", context);
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
|
||||
}
|
||||
// -> reopen the stream again
|
||||
outputStream.open(BasicFileInfo::pathForOpen(fileInfo().path()).data(), ios_base::in | ios_base::out | ios_base::binary);
|
||||
|
|
|
@ -413,7 +413,7 @@ void Mp4Tag::parse(Mp4Atom &metaAtom, Diagnostics &diag)
|
|||
try {
|
||||
child->parse(diag);
|
||||
tagField.reparse(*child, diag);
|
||||
fields().emplace(child->id(), move(tagField));
|
||||
fields().emplace(child->id(), std::move(tagField));
|
||||
} catch (const Failure &) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ void Mp4TagField::reparse(Mp4Atom &ilstChild, Diagnostics &diag)
|
|||
const auto coverSize = static_cast<streamoff>(dataAtom->dataSize() - 8);
|
||||
auto coverData = make_unique<char[]>(static_cast<size_t>(coverSize));
|
||||
stream.read(coverData.get(), coverSize);
|
||||
val->assignData(move(coverData), static_cast<size_t>(coverSize), TagDataType::Picture);
|
||||
val->assignData(std::move(coverData), static_cast<size_t>(coverSize), TagDataType::Picture);
|
||||
break;
|
||||
}
|
||||
case RawDataType::BeSignedInt: {
|
||||
|
@ -229,9 +229,9 @@ void Mp4TagField::reparse(Mp4Atom &ilstChild, Diagnostics &diag)
|
|||
auto data = make_unique<char[]>(static_cast<size_t>(dataSize));
|
||||
stream.read(data.get(), dataSize);
|
||||
if (ilstChild.id() == Mp4TagAtomIds::Cover) {
|
||||
val->assignData(move(data), static_cast<size_t>(dataSize), TagDataType::Picture);
|
||||
val->assignData(std::move(data), static_cast<size_t>(dataSize), TagDataType::Picture);
|
||||
} else {
|
||||
val->assignData(move(data), static_cast<size_t>(dataSize), TagDataType::Undefined);
|
||||
val->assignData(std::move(data), static_cast<size_t>(dataSize), TagDataType::Undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -257,7 +257,7 @@ inline Mp4TagField::IdentifierType Mp4TagField::fieldIdFromString(std::string_vi
|
|||
const auto latin1 = CppUtilities::convertUtf8ToLatin1(idString.data(), idString.size());
|
||||
switch (latin1.second) {
|
||||
case 4:
|
||||
return CppUtilities::BE::toUInt32(latin1.first.get());
|
||||
return CppUtilities::BE::toInt<std::uint32_t>(latin1.first.get());
|
||||
default:
|
||||
throw CppUtilities::ConversionException("MP4 ID must be exactly 4 chars");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
|
||||
|
||||
#include "./mp4track.h"
|
||||
#include "./mp4atom.h"
|
||||
#include "./mp4container.h"
|
||||
|
@ -1335,29 +1333,36 @@ void Mp4Track::makeMedia(Diagnostics &diag)
|
|||
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.mdhdDuration));
|
||||
}
|
||||
// convert and write language
|
||||
const std::string &language = m_locale.abbreviatedName(LocaleFormat::ISO_639_2_T, LocaleFormat::Unknown);
|
||||
std::uint16_t codedLanguage = 0;
|
||||
for (size_t charIndex = 0; charIndex != 3; ++charIndex) {
|
||||
const char langChar = charIndex < language.size() ? language[charIndex] : 0;
|
||||
// note: Not using m_locale.abbreviatedName() here to preserve "und" (explicitly undefined).
|
||||
const auto *language = static_cast<const std::string *>(&LocaleDetail::getEmpty());
|
||||
for (const auto &detail : m_locale) {
|
||||
if (!detail.empty() && (detail.format == LocaleFormat::ISO_639_2_T || detail.format == LocaleFormat::Unknown)) {
|
||||
language = &detail;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto codedLanguage = static_cast<std::uint16_t>(0u);
|
||||
for (auto charIndex = static_cast<std::size_t>(0); charIndex != 3; ++charIndex) {
|
||||
const char langChar = charIndex < language->size() ? (*language)[charIndex] : 0;
|
||||
if (langChar >= 'a' && langChar <= 'z') {
|
||||
codedLanguage |= static_cast<std::uint16_t>((langChar - 0x60) << (0xA - charIndex * 0x5));
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle invalid characters
|
||||
if (language.empty()) {
|
||||
// preserve empty language field
|
||||
if (language->empty()) {
|
||||
// preserve null value (empty language field) which is not the same as "und" (explicitly undefined)
|
||||
codedLanguage = 0;
|
||||
break;
|
||||
}
|
||||
diag.emplace_back(
|
||||
DiagLevel::Warning, "Assigned language \"" % language + "\" is of an invalid format. Setting language to undefined.", "making mdhd atom");
|
||||
diag.emplace_back(DiagLevel::Warning, "Assigned language \"" % *language + "\" is of an invalid format. Setting language to undefined.",
|
||||
"making mdhd atom");
|
||||
codedLanguage = 0x55C4; // und(efined)
|
||||
break;
|
||||
}
|
||||
if (language.size() > 3) {
|
||||
if (language->size() > 3) {
|
||||
diag.emplace_back(
|
||||
DiagLevel::Warning, "Assigned language \"" % language + "\" is longer than 3 byte and hence will be truncated.", "making mdhd atom");
|
||||
DiagLevel::Warning, "Assigned language \"" % *language + "\" is longer than 3 byte and hence will be truncated.", "making mdhd atom");
|
||||
}
|
||||
writer().writeUInt16BE(codedLanguage);
|
||||
writer().writeUInt16BE(0); // pre defined
|
||||
|
@ -1588,12 +1593,14 @@ void Mp4Track::internalParseHeader(Diagnostics &diag, AbortableProgressFeedback
|
|||
m_rawTkhdCreationTime = reader.readUInt32BE();
|
||||
m_rawTkhdModificationTime = reader.readUInt32BE();
|
||||
m_id = reader.readUInt32BE();
|
||||
m_istream->seekg(4, std::ios_base::cur);
|
||||
m_rawTkhdDuration = reader.readUInt32BE();
|
||||
break;
|
||||
case 1:
|
||||
m_rawTkhdCreationTime = reader.readUInt64BE();
|
||||
m_rawTkhdModificationTime = reader.readUInt64BE();
|
||||
m_id = reader.readUInt32BE();
|
||||
m_istream->seekg(4, std::ios_base::cur);
|
||||
m_rawTkhdDuration = reader.readUInt64BE();
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -74,7 +74,7 @@ void MpegAudioFrame::parseHeader(BinaryReader &reader, Diagnostics &diag)
|
|||
m_xingBytesfield = reader.readUInt32BE();
|
||||
}
|
||||
if (isXingTocFieldPresent()) {
|
||||
reader.stream()->seekg(64, ios_base::cur);
|
||||
reader.stream()->seekg(0x64, ios_base::cur);
|
||||
}
|
||||
if (isXingQualityIndicatorFieldPresent()) {
|
||||
m_xingQualityIndicator = reader.readUInt32BE();
|
||||
|
|
|
@ -174,7 +174,7 @@ constexpr XingHeaderFlags MpegAudioFrame::xingHeaderFlags() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingFramefieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -182,7 +182,7 @@ constexpr bool MpegAudioFrame::isXingFramefieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingBytesfieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasFramesField) == XingHeaderFlags::HasFramesField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasBytesField) == XingHeaderFlags::HasBytesField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -190,7 +190,7 @@ constexpr bool MpegAudioFrame::isXingBytesfieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingTocFieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasTocField) == XingHeaderFlags::HasTocField) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasTocField) == XingHeaderFlags::HasTocField);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -198,7 +198,7 @@ constexpr bool MpegAudioFrame::isXingTocFieldPresent() const
|
|||
*/
|
||||
constexpr bool MpegAudioFrame::isXingQualityIndicatorFieldPresent() const
|
||||
{
|
||||
return (isXingHeaderAvailable()) ? ((m_xingHeaderFlags & XingHeaderFlags::HasQualityIndicator) == XingHeaderFlags::HasQualityIndicator) : false;
|
||||
return isXingHeaderAvailable() && ((m_xingHeaderFlags & XingHeaderFlags::HasQualityIndicator) == XingHeaderFlags::HasQualityIndicator);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "../exceptions.h"
|
||||
#include "../mediaformat.h"
|
||||
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
using namespace std;
|
||||
|
@ -35,20 +37,13 @@ void MpegAudioFrameStream::internalParseHeader(Diagnostics &diag, AbortableProgr
|
|||
if (!m_istream) {
|
||||
throw NoDataFoundException();
|
||||
}
|
||||
// get size
|
||||
m_istream->seekg(-128, ios_base::end);
|
||||
if (m_reader.readUInt24BE() == 0x544147) {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) - 3u - m_startOffset;
|
||||
} else {
|
||||
m_size = static_cast<std::uint64_t>(m_istream->tellg()) + 125u - m_startOffset;
|
||||
}
|
||||
m_istream->seekg(static_cast<streamoff>(m_startOffset), ios_base::beg);
|
||||
// parse frames until the first valid, non-empty frame is reached
|
||||
m_istream->seekg(static_cast<std::streamoff>(m_startOffset), ios_base::beg);
|
||||
for (size_t invalidByteskipped = 0; m_frames.size() < 200 && invalidByteskipped <= 0x600u;) {
|
||||
MpegAudioFrame &frame = invalidByteskipped > 0 ? m_frames.back() : m_frames.emplace_back();
|
||||
try {
|
||||
frame.parseHeader(m_reader, diag);
|
||||
} catch (const InvalidDataException &e) {
|
||||
} catch (const InvalidDataException &) {
|
||||
if (++invalidByteskipped > 1) {
|
||||
diag.pop_back();
|
||||
}
|
||||
|
@ -73,18 +68,24 @@ void MpegAudioFrameStream::internalParseHeader(Diagnostics &diag, AbortableProgr
|
|||
const MpegAudioFrame &frame = m_frames.back();
|
||||
addInfo(frame, *this);
|
||||
if (frame.isXingBytesfieldPresent()) {
|
||||
std::uint32_t xingSize = frame.xingBytesfield();
|
||||
if (m_size && xingSize != m_size) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
"Real length of MPEG audio frames is not in accordance with value provided by Xing header. The Xing header value will be used.",
|
||||
context);
|
||||
const auto xingSize = frame.xingBytesfield();
|
||||
if (!m_size) {
|
||||
m_size = xingSize;
|
||||
} else if (xingSize != m_size) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Real size of MPEG audio frames (", m_size, " byte) is not in accordance with value provided by Xing header (", xingSize,
|
||||
" byte). The real size will be used."),
|
||||
context);
|
||||
}
|
||||
}
|
||||
m_bitrate = frame.isXingFramefieldPresent() ? ((static_cast<double>(m_size) * 8.0)
|
||||
/ (static_cast<double>(frame.xingFrameCount() * frame.sampleCount()) / static_cast<double>(frame.samplingFrequency())) / 1024.0)
|
||||
: frame.bitrate();
|
||||
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bytesPerSecond = static_cast<std::uint32_t>(m_bitrate * 125)));
|
||||
if (frame.isXingFramefieldPresent()) {
|
||||
const auto duration = static_cast<double>(frame.xingFrameCount() * frame.sampleCount()) / static_cast<double>(frame.samplingFrequency());
|
||||
m_bitrate = static_cast<double>(m_size) / duration / 125.0;
|
||||
m_duration = TimeSpan::fromSeconds(duration);
|
||||
} else {
|
||||
m_bitrate = frame.bitrate();
|
||||
m_duration = TimeSpan::fromSeconds(static_cast<double>(m_size) / (m_bytesPerSecond = static_cast<std::uint32_t>(m_bitrate * 125)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -280,6 +280,10 @@ void OggContainer::internalParseTags(Diagnostics &diag, AbortableProgressFeedbac
|
|||
{
|
||||
// tracks needs to be parsed before because tags are stored at stream level
|
||||
parseTracks(diag, progress);
|
||||
auto flags = VorbisCommentFlags::None;
|
||||
if (fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::ConvertTotalFields) {
|
||||
flags += VorbisCommentFlags::ConvertTotalFields;
|
||||
}
|
||||
for (auto &comment : m_tags) {
|
||||
OggParameter ¶ms = comment->oggParams();
|
||||
m_iterator.setPageIndex(params.firstPageIndex);
|
||||
|
@ -287,16 +291,16 @@ void OggContainer::internalParseTags(Diagnostics &diag, AbortableProgressFeedbac
|
|||
m_iterator.setFilter(m_iterator.currentPage().streamSerialNumber());
|
||||
switch (params.streamFormat) {
|
||||
case GeneralMediaFormat::Vorbis:
|
||||
comment->parse(m_iterator, VorbisCommentFlags::None, diag);
|
||||
comment->parse(m_iterator, flags, diag);
|
||||
break;
|
||||
case GeneralMediaFormat::Opus:
|
||||
// skip header (has already been detected by OggStream)
|
||||
m_iterator.ignore(8);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
break;
|
||||
case GeneralMediaFormat::Flac:
|
||||
m_iterator.ignore(4);
|
||||
comment->parse(m_iterator, VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
comment->parse(m_iterator, flags | VorbisCommentFlags::NoSignature | VorbisCommentFlags::NoFramingByte, diag);
|
||||
break;
|
||||
default:
|
||||
diag.emplace_back(DiagLevel::Critical, "Stream format not supported.", "parsing tags from OGG streams");
|
||||
|
@ -435,11 +439,11 @@ void OggContainer::internalMakeFile(Diagnostics &diag, AbortableProgressFeedback
|
|||
}
|
||||
|
||||
// define misc variables
|
||||
CopyHelper<65307> copyHelper;
|
||||
vector<std::uint64_t> updatedPageOffsets;
|
||||
const OggPage *lastPage = nullptr;
|
||||
std::uint64_t nextPageOffset;
|
||||
unordered_map<std::uint32_t, std::uint32_t> pageSequenceNumberBySerialNo;
|
||||
auto copyHelper = CopyHelper<65307>();
|
||||
auto updatedPageOffsets = std::vector<std::uint64_t>();
|
||||
auto nextPageOffset = std::uint64_t();
|
||||
auto pageSequenceNumberBySerialNo = std::unordered_map<std::uint32_t, std::uint32_t>();
|
||||
|
||||
// iterate through all pages of the original file
|
||||
auto updateTick = 0u;
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
|
||||
/// \cond
|
||||
namespace CppUtilities {
|
||||
template <std::size_t bufferSize> class CopyHelper;
|
||||
}
|
||||
/// \endcond
|
||||
|
||||
namespace TagParser {
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ void OggPage::parseHeader(istream &stream, std::uint64_t startOffset, std::int32
|
|||
if (++i < m_segmentCount && entry < 0xFF) {
|
||||
m_segmentSizes.emplace_back(0);
|
||||
} else if (i == m_segmentCount && entry == 0xFF) {
|
||||
m_headerTypeFlag |= 0x80; // FIXME v11: don't abuse header type flags
|
||||
m_lastSegmentUnconcluded = true;
|
||||
}
|
||||
}
|
||||
// check whether the maximum size is exceeded
|
||||
|
|
|
@ -48,6 +48,7 @@ private:
|
|||
std::uint32_t m_sequenceNumber;
|
||||
std::uint32_t m_checksum;
|
||||
std::uint8_t m_segmentCount;
|
||||
bool m_lastSegmentUnconcluded;
|
||||
std::vector<std::uint32_t> m_segmentSizes;
|
||||
};
|
||||
|
||||
|
@ -63,6 +64,7 @@ inline OggPage::OggPage()
|
|||
, m_sequenceNumber(0)
|
||||
, m_checksum(0)
|
||||
, m_segmentCount(0)
|
||||
, m_lastSegmentUnconcluded(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ inline bool OggPage::isLastPage() const
|
|||
*/
|
||||
inline bool OggPage::isLastSegmentUnconcluded() const
|
||||
{
|
||||
return m_headerTypeFlag & 0x80;
|
||||
return m_lastSegmentUnconcluded;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -25,15 +25,15 @@ void OpusIdentificationHeader::parseHeader(OggIterator &iterator)
|
|||
{
|
||||
char buff[19 - 8];
|
||||
iterator.read(buff, 8);
|
||||
if (BE::toUInt64(buff) != 0x4F70757348656164u) {
|
||||
if (BE::toInt<std::uint64_t>(buff) != 0x4F70757348656164u) {
|
||||
throw InvalidDataException(); // not Opus identification header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
m_version = static_cast<std::uint8_t>(*(buff));
|
||||
m_channels = static_cast<std::uint8_t>(*(buff + 1));
|
||||
m_preSkip = LE::toUInt16(buff + 2);
|
||||
m_preSkip = LE::toInt<std::uint16_t>(buff + 2);
|
||||
m_sampleRate = LE::toUInt32(buff + 4);
|
||||
m_outputGain = LE::toUInt16(buff + 8);
|
||||
m_outputGain = LE::toInt<std::uint16_t>(buff + 8);
|
||||
m_channelMap = static_cast<std::uint8_t>(*(buff + 10));
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ public:
|
|||
PositionInSet(const StringType &numericString);
|
||||
|
||||
constexpr std::int32_t position() const;
|
||||
void setPosition(std::int32_t position);
|
||||
constexpr std::int32_t total() const;
|
||||
void setTotal(std::int32_t total);
|
||||
constexpr bool isNull() const;
|
||||
constexpr bool operator==(const PositionInSet &other) const;
|
||||
|
||||
|
@ -80,6 +82,14 @@ constexpr inline std::int32_t PositionInSet::position() const
|
|||
return m_position;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the element position of the current instance.
|
||||
*/
|
||||
inline void PositionInSet::setPosition(int32_t position)
|
||||
{
|
||||
m_position = position;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns the total element count of the current instance.
|
||||
*/
|
||||
|
@ -88,6 +98,14 @@ constexpr inline std::int32_t PositionInSet::total() const
|
|||
return m_total;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the total element count of the current instance.
|
||||
*/
|
||||
inline void PositionInSet::setTotal(int32_t total)
|
||||
{
|
||||
m_total = total;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an indication whether both the element position and total element count is 0.
|
||||
*/
|
||||
|
|
|
@ -187,6 +187,7 @@ class AbortableProgressFeedback : public BasicProgressFeedback<AbortableProgress
|
|||
public:
|
||||
explicit AbortableProgressFeedback(const Callback &callback, const Callback &percentageOnlyCallback = Callback());
|
||||
explicit AbortableProgressFeedback(Callback &&callback = Callback(), Callback &&percentageOnlyCallback = Callback());
|
||||
AbortableProgressFeedback(const AbortableProgressFeedback &);
|
||||
|
||||
bool isAborted() const;
|
||||
void tryToAbort();
|
||||
|
@ -220,6 +221,15 @@ inline AbortableProgressFeedback::AbortableProgressFeedback(Callback &&callback,
|
|||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new AbortableProgressFeedback based on \a other.
|
||||
*/
|
||||
inline AbortableProgressFeedback::AbortableProgressFeedback(const AbortableProgressFeedback &other)
|
||||
: BasicProgressFeedback<AbortableProgressFeedback>(other)
|
||||
, m_aborted(other.isAborted())
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns whether the operation has been aborted via tryToAbort().
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace TagParser {
|
|||
* \brief Holds 64-bit signatures.
|
||||
*/
|
||||
enum Sig64 : std::uint64_t {
|
||||
ApeTag = 0x4150455441474558ul, // APETAGEX
|
||||
Ar = 0x213C617263683E0A,
|
||||
Asf1 = 0x3026B2758E66CF11ul,
|
||||
Asf2 = 0xA6D900AA0062CE6Cul,
|
||||
|
@ -38,6 +39,13 @@ enum Sig48 : std::uint64_t {
|
|||
Xz = 0xFD377A585A00ul,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Holds 40-bit signatures.
|
||||
*/
|
||||
enum Sig40 : std::uint64_t {
|
||||
Aiff = 0x464F524D00ul,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Holds 32-bit signatures.
|
||||
*/
|
||||
|
@ -65,6 +73,7 @@ enum Sig32 : std::uint32_t {
|
|||
Zip1 = 0x504B0304u,
|
||||
Zip2 = 0x504B0506u,
|
||||
Zip3 = 0x504B0708u,
|
||||
Zstd = 0x28b52ffdu,
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -106,18 +115,20 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
// read signature
|
||||
std::uint64_t sig = 0;
|
||||
if (buffer.size() >= 8) {
|
||||
sig = BE::toUInt64(buffer.data());
|
||||
sig = BE::toInt<std::uint64_t>(buffer.data());
|
||||
} else if (buffer.size() >= 4) {
|
||||
sig = BE::toUInt32(buffer.data());
|
||||
sig = BE::toInt<std::uint32_t>(buffer.data());
|
||||
sig <<= 4;
|
||||
} else if (buffer.size() >= 2) {
|
||||
sig = BE::toUInt16(buffer.data());
|
||||
sig = BE::toInt<std::uint16_t>(buffer.data());
|
||||
sig <<= 6;
|
||||
} else {
|
||||
return ContainerFormat::Unknown;
|
||||
}
|
||||
// return corresponding container format
|
||||
switch (sig) { // check 64-bit signatures
|
||||
case ApeTag:
|
||||
return ContainerFormat::ApeTag;
|
||||
case Ar:
|
||||
return ContainerFormat::Ar;
|
||||
case Asf1:
|
||||
|
@ -156,6 +167,11 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
return ContainerFormat::Xz;
|
||||
default:;
|
||||
}
|
||||
switch (sig >> 24) { // check 40-bit signatures
|
||||
case Aiff:
|
||||
return ContainerFormat::Aiff;
|
||||
default:;
|
||||
}
|
||||
switch (sig >> 32) { // check 32-bit signatures
|
||||
case Dirac:
|
||||
return ContainerFormat::Dirac;
|
||||
|
@ -178,9 +194,9 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
case PhotoshopDocument:
|
||||
return ContainerFormat::PhotoshopDocument;
|
||||
case Riff:
|
||||
if (buffer.size() >= 16 && BE::toUInt64(buffer.data() + 8) == Sig64::RiffAvi) {
|
||||
if (buffer.size() >= 16 && BE::toInt<std::uint64_t>(buffer.data() + 8) == Sig64::RiffAvi) {
|
||||
return ContainerFormat::RiffAvi;
|
||||
} else if (buffer.size() >= 12 && BE::toUInt32(buffer.data() + 8) == RiffWave) {
|
||||
} else if (buffer.size() >= 12 && BE::toInt<std::uint32_t>(buffer.data() + 8) == RiffWave) {
|
||||
return ContainerFormat::RiffWave;
|
||||
} else {
|
||||
return ContainerFormat::Riff;
|
||||
|
@ -201,6 +217,8 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
case Zip2:
|
||||
case Zip3:
|
||||
return ContainerFormat::Zip;
|
||||
case Zstd:
|
||||
return ContainerFormat::Zstd;
|
||||
default:;
|
||||
}
|
||||
switch (sig >> 40) { // check 24-bit signatures
|
||||
|
@ -211,7 +229,7 @@ ContainerFormat parseSignature(std::string_view buffer)
|
|||
case Gzip:
|
||||
return ContainerFormat::Gzip;
|
||||
case Id3v2:
|
||||
return ContainerFormat::Id2v2Tag;
|
||||
return ContainerFormat::Id3v2Tag;
|
||||
case Utf8Text:
|
||||
return ContainerFormat::Utf8Text;
|
||||
}
|
||||
|
@ -360,6 +378,10 @@ std::string_view containerFormatAbbreviation(ContainerFormat containerFormat, Me
|
|||
return "ape";
|
||||
case ContainerFormat::Midi:
|
||||
return "mid";
|
||||
case ContainerFormat::Aiff:
|
||||
return "aiff";
|
||||
case ContainerFormat::Zstd:
|
||||
return "zst";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -465,6 +487,14 @@ std::string_view containerFormatName(ContainerFormat containerFormat)
|
|||
return "Monkey's Audio";
|
||||
case ContainerFormat::Midi:
|
||||
return "MIDI";
|
||||
case ContainerFormat::Aiff:
|
||||
return "Audio Interchange File Format";
|
||||
case ContainerFormat::Zstd:
|
||||
return "Zstandard compressed file";
|
||||
case ContainerFormat::Id3v2Tag:
|
||||
return "ID3v2 tag";
|
||||
case ContainerFormat::ApeTag:
|
||||
return "APE tag";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
|
@ -561,6 +591,8 @@ std::string_view containerMimeType(ContainerFormat containerFormat, MediaType me
|
|||
return "image/bmp";
|
||||
case ContainerFormat::WindowsIcon:
|
||||
return "image/vnd.microsoft.icon";
|
||||
case ContainerFormat::Zstd:
|
||||
return "application/zstd";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ enum class ContainerFormat : unsigned int {
|
|||
Gif87a, /**< Graphics Interchange Format (1987) */
|
||||
Gif89a, /**< Graphics Interchange Format (1989) */
|
||||
Gzip, /**< gzip compressed file */
|
||||
Id2v2Tag, /**< file holding an ID2v2 tag only */
|
||||
Id3v2Tag, /**< file holding an ID3v2 tag only */
|
||||
Ivf, /**< IVF (simple file format that transports raw VP8/VP9/AV1 data) */
|
||||
JavaClassFile, /**< Java class file */
|
||||
Jpeg, /**< JPEG File Interchange Format */
|
||||
|
@ -65,6 +65,9 @@ enum class ContainerFormat : unsigned int {
|
|||
Xz, /**< xz compressed file */
|
||||
YUV4Mpeg2, /**< YUV4MPEG2 */
|
||||
Zip, /**< ZIP archive */
|
||||
Aiff, /**< Audio Interchange File Format */
|
||||
Zstd, /**< Zstandard-compressed data */
|
||||
ApeTag, /**< APE tag */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT ContainerFormat parseSignature(const char *buffer, std::size_t bufferSize);
|
||||
|
|
3
tag.cpp
3
tag.cpp
|
@ -4,6 +4,9 @@ using namespace std;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The TagPrivate struct contains private fields of the Tag class.
|
||||
struct TagPrivate {};
|
||||
|
||||
/*!
|
||||
* \class TagParser::Tag
|
||||
* \brief The Tag class is used to store, read and write tag information.
|
||||
|
|
7
tag.h
7
tag.h
|
@ -8,6 +8,7 @@
|
|||
#include <c++utilities/io/binaryreader.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
|
@ -125,6 +126,7 @@ enum class KnownField : unsigned int {
|
|||
ProductionCopyright, /** production copyright */
|
||||
License, /** license */
|
||||
TermsOfUse, /** terms of use */
|
||||
PublisherWebpage, /** the publisher's official webpage */
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -135,7 +137,7 @@ constexpr KnownField firstKnownField = KnownField::Title;
|
|||
/*!
|
||||
* \brief The last valid entry in the TagParser::KnownField enum.
|
||||
*/
|
||||
constexpr KnownField lastKnownField = KnownField::TermsOfUse;
|
||||
constexpr KnownField lastKnownField = KnownField::PublisherWebpage;
|
||||
|
||||
/*!
|
||||
* \brief The number of valid entries in the TagParser::KnownField enum.
|
||||
|
@ -160,6 +162,8 @@ constexpr KnownField nextKnownField(KnownField field)
|
|||
return isKnownFieldDeprecated(next) ? nextKnownField(next) : next;
|
||||
}
|
||||
|
||||
struct TagPrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT Tag {
|
||||
public:
|
||||
virtual ~Tag();
|
||||
|
@ -199,6 +203,7 @@ protected:
|
|||
|
||||
std::string m_version;
|
||||
std::uint64_t m_size;
|
||||
std::unique_ptr<TagPrivate> m_p;
|
||||
TagTarget m_target;
|
||||
};
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ std::string TagTarget::toString(TagTargetLevel tagTargetLevel) const
|
|||
if (levelString.empty()) {
|
||||
parts.emplace_back("undefined target");
|
||||
} else {
|
||||
parts.emplace_back(move(levelString));
|
||||
parts.emplace_back(std::move(levelString));
|
||||
}
|
||||
for (auto v : tracks()) {
|
||||
parts.emplace_back("track " + numberToString(v));
|
||||
|
|
217
tagvalue.cpp
217
tagvalue.cpp
|
@ -22,6 +22,9 @@ using namespace CppUtilities;
|
|||
|
||||
namespace TagParser {
|
||||
|
||||
/// \brief The TagValuePrivate struct contains private fields of the TagValue class.
|
||||
struct TagValuePrivate {};
|
||||
|
||||
/*!
|
||||
* \brief Returns the string representation of the specified \a dataType.
|
||||
*/
|
||||
|
@ -48,6 +51,8 @@ std::string_view tagDataTypeString(TagDataType dataType)
|
|||
return "popularity";
|
||||
case TagDataType::UnsignedInteger:
|
||||
return "unsigned integer";
|
||||
case TagDataType::DateTimeExpression:
|
||||
return "date time expression";
|
||||
default:
|
||||
return "undefined";
|
||||
}
|
||||
|
@ -72,6 +77,13 @@ pair<const char *, float> encodingParameter(TagTextEncoding tagTextEncoding)
|
|||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \class TagParser::Popularity
|
||||
* \brief The Popularity class contains a value for ID3v2's "Popularimeter" field.
|
||||
* \remarks It can also be used for other formats than ID3v2.
|
||||
* \sa See documentation of TagParser::Popularity for scaling.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \class TagParser::TagValue
|
||||
* \brief The TagValue class wraps values of different types. It is meant to be assigned to a tag field.
|
||||
|
@ -89,7 +101,7 @@ pair<const char *, float> encodingParameter(TagTextEncoding tagTextEncoding)
|
|||
*
|
||||
* Values of the type TagDataType::Text can be differently encoded.
|
||||
* - See TagParser::TagTextEncoding for a list of encodings supported by this library.
|
||||
* - Tag formats usually only support a subset of these encodings. The serializers for the varoius tag
|
||||
* - Tag formats usually only support a subset of these encodings. The serializers for the various tag
|
||||
* formats provided by this library will keep the encoding if possible and otherwise convert the assigned
|
||||
* text to an encoding supported by the tag format on the fly. Note that ID3v1 does not specify which
|
||||
* encodings are supported (or unsupported) so the serializer will just write text data as-is.
|
||||
|
@ -136,6 +148,140 @@ TagValue::TagValue(const TagValue &other)
|
|||
}
|
||||
}
|
||||
|
||||
TagValue::TagValue(TagValue &&other) = default;
|
||||
|
||||
/*!
|
||||
* \brief Constructs an empty TagValue.
|
||||
*/
|
||||
TagValue::TagValue()
|
||||
: m_size(0)
|
||||
, m_type(TagDataType::Undefined)
|
||||
, m_encoding(TagTextEncoding::Latin1)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.)
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textSize, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned. This string must be null-terminated.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const char *text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
{
|
||||
assignText(text, std::strlen(text), textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(const std::string &text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
TagValue::TagValue(std::string_view text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the TagValue.
|
||||
*/
|
||||
TagValue::~TagValue()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue with a copy of the given \a data.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Strips the BOM of the specified \a data if \a type is TagDataType::Text.
|
||||
*/
|
||||
TagValue::TagValue(const char *data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
if (type == TagDataType::Text) {
|
||||
stripBom(data, m_size, encoding);
|
||||
}
|
||||
m_ptr = std::make_unique<char[]>(m_size);
|
||||
std::copy(data, data + m_size, m_ptr.get());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding with the given \a data.
|
||||
*
|
||||
* The \a data is not copied. It is moved.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present.
|
||||
*/
|
||||
TagValue::TagValue(std::unique_ptr<char[]> &&data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
m_ptr = std::move(data);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Assigns the value of another TagValue to the current instance.
|
||||
*/
|
||||
|
@ -161,6 +307,8 @@ TagValue &TagValue::operator=(const TagValue &other)
|
|||
return *this;
|
||||
}
|
||||
|
||||
TagValue &TagValue::operator=(TagValue &&other) = default;
|
||||
|
||||
/// \cond
|
||||
TagTextEncoding pickUtfEncoding(TagTextEncoding encoding1, TagTextEncoding encoding2)
|
||||
{
|
||||
|
@ -196,8 +344,6 @@ TagTextEncoding pickUtfEncoding(TagTextEncoding encoding1, TagTextEncoding encod
|
|||
* - If any of the differently typed values can not be converted to a string (eg. it is binary data) the values
|
||||
* are *not* considered equal. So the text "foo" and the binary value "foo" are not considered equal although
|
||||
* the raw data is identical.
|
||||
* - In fact, values of the types TagDataType::DateTime, TagDataType::TimeSpan, TagDataType::Picture, TagDataType::Binary
|
||||
* and TagDataType::Unspecified will never be considered equal with a value of another type.
|
||||
* - If the type is TagDataType::Text and the encoding differs values might still be considered equal if they
|
||||
* represent the same characters. The same counts for the description.
|
||||
* - This might be a costly operation due to possible conversions.
|
||||
|
@ -296,6 +442,8 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options
|
|||
return toTimeSpan() == other.toTimeSpan();
|
||||
case TagDataType::DateTime:
|
||||
return toDateTime() == other.toDateTime();
|
||||
case TagDataType::DateTimeExpression:
|
||||
return toDateTimeExpression() == other.toDateTimeExpression();
|
||||
case TagDataType::Picture:
|
||||
case TagDataType::Binary:
|
||||
case TagDataType::Undefined:
|
||||
|
@ -311,6 +459,7 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options
|
|||
switch (dataType) {
|
||||
case TagDataType::TimeSpan:
|
||||
case TagDataType::DateTime:
|
||||
case TagDataType::DateTimeExpression:
|
||||
case TagDataType::Picture:
|
||||
case TagDataType::Binary:
|
||||
case TagDataType::Undefined:
|
||||
|
@ -588,13 +737,13 @@ TimeSpan TagValue::toTimeSpan() const
|
|||
case sizeof(std::int64_t):
|
||||
return TimeSpan(*(reinterpret_cast<std::int64_t *>(m_ptr.get())));
|
||||
default:
|
||||
throw ConversionException("The size of the assigned integer is not appropriate for conversion to time span.");
|
||||
throw ConversionException("The size of the assigned data is not appropriate for conversion to time span.");
|
||||
}
|
||||
case TagDataType::UnsignedInteger:
|
||||
switch (m_size) {
|
||||
case sizeof(std::uint64_t): {
|
||||
const auto ticks = *(reinterpret_cast<std::uint64_t *>(m_ptr.get()));
|
||||
if (ticks < std::numeric_limits<std::int64_t>::max()) {
|
||||
if (ticks < static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
|
||||
return TimeSpan(static_cast<std::int64_t>(ticks));
|
||||
}
|
||||
}
|
||||
|
@ -608,7 +757,7 @@ TimeSpan TagValue::toTimeSpan() const
|
|||
|
||||
/*!
|
||||
* \brief Converts the value of the current TagValue object to its equivalent
|
||||
* DateTime representation.
|
||||
* DateTime representation (using the UTC timezone).
|
||||
* \throws Throws ConversionException on failure.
|
||||
*/
|
||||
DateTime TagValue::toDateTime() const
|
||||
|
@ -633,7 +782,43 @@ DateTime TagValue::toDateTime() const
|
|||
} else if (m_size == sizeof(std::uint64_t)) {
|
||||
return DateTime(*(reinterpret_cast<std::uint64_t *>(m_ptr.get())));
|
||||
} else {
|
||||
throw ConversionException("The size of the assigned integer is not appropriate for conversion to date time.");
|
||||
throw ConversionException("The size of the assigned data is not appropriate for conversion to date time.");
|
||||
}
|
||||
case TagDataType::DateTimeExpression:
|
||||
return toDateTimeExpression().gmt();
|
||||
default:
|
||||
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time."));
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Converts the value of the current TagValue object to its equivalent
|
||||
* DateTimeExpression representation.
|
||||
* \throws Throws ConversionException on failure.
|
||||
*/
|
||||
CppUtilities::DateTimeExpression TagParser::TagValue::toDateTimeExpression() const
|
||||
{
|
||||
if (isEmpty()) {
|
||||
return DateTimeExpression();
|
||||
}
|
||||
switch (m_type) {
|
||||
case TagDataType::Text: {
|
||||
const auto str = toString(m_encoding == TagTextEncoding::Utf8 ? TagTextEncoding::Utf8 : TagTextEncoding::Latin1);
|
||||
try {
|
||||
return DateTimeExpression::fromIsoString(str.data());
|
||||
} catch (const ConversionException &) {
|
||||
return DateTimeExpression::fromString(str.data());
|
||||
}
|
||||
}
|
||||
case TagDataType::Integer:
|
||||
case TagDataType::DateTime:
|
||||
case TagDataType::UnsignedInteger:
|
||||
return DateTimeExpression{ .value = toDateTime(), .delta = TimeSpan(), .parts = DateTimeParts::DateTime };
|
||||
case TagDataType::DateTimeExpression:
|
||||
if (m_size == sizeof(DateTimeExpression)) {
|
||||
return *reinterpret_cast<DateTimeExpression *>(m_ptr.get());
|
||||
} else {
|
||||
throw ConversionException("The size of the assigned data is not appropriate for conversion to date time expression.");
|
||||
}
|
||||
default:
|
||||
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time."));
|
||||
|
@ -671,7 +856,11 @@ Popularity TagValue::toPopularity() const
|
|||
auto reader = BinaryReader(&s);
|
||||
try {
|
||||
s.exceptions(std::ios_base::failbit | std::ios_base::badbit);
|
||||
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
|
||||
s.rdbuf()->pubsetbuf(m_ptr.get(), static_cast<std::streamsize>(m_size));
|
||||
#else
|
||||
s.write(m_ptr.get(), static_cast<std::streamsize>(m_size));
|
||||
#endif
|
||||
popularity.user = reader.readLengthPrefixedString();
|
||||
popularity.rating = reader.readFloat64LE();
|
||||
popularity.playCounter = reader.readUInt64LE();
|
||||
|
@ -893,7 +1082,7 @@ void TagValue::toString(string &result, TagTextEncoding encoding) const
|
|||
result = toTimeSpan().toString();
|
||||
break;
|
||||
case TagDataType::DateTime:
|
||||
result = toDateTime().toString(DateTimeOutputFormat::IsoOmittingDefaultComponents);
|
||||
result = toDateTime().toIsoString();
|
||||
break;
|
||||
case TagDataType::Popularity:
|
||||
result = toPopularity().toString();
|
||||
|
@ -901,6 +1090,9 @@ void TagValue::toString(string &result, TagTextEncoding encoding) const
|
|||
case TagDataType::UnsignedInteger:
|
||||
result = numberToString(toUnsignedInteger());
|
||||
break;
|
||||
case TagDataType::DateTimeExpression:
|
||||
result = toDateTimeExpression().toIsoString();
|
||||
break;
|
||||
default:
|
||||
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
|
||||
}
|
||||
|
@ -991,6 +1183,9 @@ void TagValue::toWString(std::u16string &result, TagTextEncoding encoding) const
|
|||
case TagDataType::UnsignedInteger:
|
||||
regularStrRes = numberToString(toUnsignedInteger());
|
||||
break;
|
||||
case TagDataType::DateTimeExpression:
|
||||
regularStrRes = toDateTimeExpression().toIsoString();
|
||||
break;
|
||||
default:
|
||||
throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string."));
|
||||
}
|
||||
|
@ -1127,7 +1322,7 @@ void TagValue::assignData(unique_ptr<char[]> &&data, size_t length, TagDataType
|
|||
m_size = length;
|
||||
m_type = type;
|
||||
m_encoding = encoding;
|
||||
m_ptr = move(data);
|
||||
m_ptr = std::move(data);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -1165,13 +1360,13 @@ void TagValue::stripBom(const char *&text, size_t &length, TagTextEncoding encod
|
|||
}
|
||||
break;
|
||||
case TagTextEncoding::Utf16LittleEndian:
|
||||
if ((length >= 2) && (LE::toUInt16(text) == 0xFEFF)) {
|
||||
if ((length >= 2) && (LE::toInt<std::uint16_t>(text) == 0xFEFF)) {
|
||||
text += 2;
|
||||
length -= 2;
|
||||
}
|
||||
break;
|
||||
case TagTextEncoding::Utf16BigEndian:
|
||||
if ((length >= 2) && (BE::toUInt16(text) == 0xFEFF)) {
|
||||
if ((length >= 2) && (BE::toInt<std::uint16_t>(text) == 0xFEFF)) {
|
||||
text += 2;
|
||||
length -= 2;
|
||||
}
|
||||
|
|
165
tagvalue.h
165
tagvalue.h
|
@ -121,15 +121,18 @@ enum class TagDataType : unsigned int {
|
|||
Integer, /**< integer */
|
||||
PositionInSet, /**< position in set, see TagParser::PositionInSet */
|
||||
StandardGenreIndex, /**< pre-defined genre name denoted by numerical code */
|
||||
TimeSpan, /**< time span, see ChronoUtils::TimeSpan */
|
||||
DateTime, /**< date time, see ChronoUtils::DateTime */
|
||||
TimeSpan, /**< time span, see CppUtilities::TimeSpan */
|
||||
DateTime, /**< date time, see CppUtilities::DateTime */
|
||||
Picture, /**< picture file */
|
||||
Binary, /**< unspecified binary data */
|
||||
Undefined, /**< undefined/invalid data type */
|
||||
Popularity, /**< rating with user info and play counter (as in ID3v2's "Popularimeter") */
|
||||
UnsignedInteger, /**< unsigned integer */
|
||||
DateTimeExpression, /**< date time expression, see CppUtilities::DateTimeExpression */
|
||||
};
|
||||
|
||||
TAG_PARSER_EXPORT std::string_view tagDataTypeString(TagDataType dataType);
|
||||
|
||||
/*!
|
||||
* \brief The TagValueComparisionOption enum specifies options for TagValue::compareTo().
|
||||
*/
|
||||
|
@ -139,6 +142,8 @@ enum class TagValueComparisionFlags : unsigned int {
|
|||
IgnoreMetaData = 0x2, /**< do *not* take meta-data like description and MIME-types into account */
|
||||
};
|
||||
|
||||
struct TagValuePrivate;
|
||||
|
||||
class TAG_PARSER_EXPORT TagValue {
|
||||
public:
|
||||
// constructor, destructor
|
||||
|
@ -159,15 +164,16 @@ public:
|
|||
TagTextEncoding encoding = TagTextEncoding::Latin1);
|
||||
explicit TagValue(PositionInSet value);
|
||||
explicit TagValue(CppUtilities::DateTime value);
|
||||
explicit TagValue(const CppUtilities::DateTimeExpression &value);
|
||||
explicit TagValue(CppUtilities::TimeSpan value);
|
||||
explicit TagValue(const Popularity &value);
|
||||
TagValue(const TagValue &other);
|
||||
TagValue(TagValue &&other) = default;
|
||||
TagValue(TagValue &&other);
|
||||
~TagValue();
|
||||
|
||||
// operators
|
||||
TagValue &operator=(const TagValue &other);
|
||||
TagValue &operator=(TagValue &&other) = default;
|
||||
TagValue &operator=(TagValue &&other);
|
||||
bool operator==(const TagValue &other) const;
|
||||
bool operator!=(const TagValue &other) const;
|
||||
operator bool() const;
|
||||
|
@ -190,6 +196,7 @@ public:
|
|||
PositionInSet toPositionInSet() const;
|
||||
CppUtilities::TimeSpan toTimeSpan() const;
|
||||
CppUtilities::DateTime toDateTime() const;
|
||||
CppUtilities::DateTimeExpression toDateTimeExpression() const;
|
||||
Popularity toPopularity() const;
|
||||
Popularity toScaledPopularity(TagType scale = TagType::Unspecified) const;
|
||||
std::size_t dataSize() const;
|
||||
|
@ -231,6 +238,7 @@ public:
|
|||
void assignPosition(PositionInSet value);
|
||||
void assignTimeSpan(CppUtilities::TimeSpan value);
|
||||
void assignDateTime(CppUtilities::DateTime value);
|
||||
void assignDateTimeExpression(const CppUtilities::DateTimeExpression &value);
|
||||
void assignPopularity(const Popularity &value);
|
||||
|
||||
static void stripBom(const char *&text, std::size_t &length, TagTextEncoding encoding);
|
||||
|
@ -256,90 +264,9 @@ private:
|
|||
TagTextEncoding m_encoding;
|
||||
TagTextEncoding m_descEncoding;
|
||||
TagValueFlags m_flags;
|
||||
std::unique_ptr<TagValuePrivate> m_p;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Constructs an empty TagValue.
|
||||
*/
|
||||
inline TagValue::TagValue()
|
||||
: m_size(0)
|
||||
, m_type(TagDataType::Undefined)
|
||||
, m_encoding(TagTextEncoding::Latin1)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Destroys the TagValue.
|
||||
*/
|
||||
inline TagValue::~TagValue()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textSize Specifies the size of \a text. (The actual number of bytes, not the number of characters.)
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *text, std::size_t textSize, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textSize, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned. This string must be null-terminated.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
{
|
||||
assignText(text, std::strlen(text), textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(const std::string &text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given \a text.
|
||||
* \param text Specifies the text to be assigned.
|
||||
* \param textEncoding Specifies the encoding of the given \a text.
|
||||
* \param convertTo Specifies the encoding to convert \a text to; set to TagTextEncoding::Unspecified to
|
||||
* use \a textEncoding without any character set conversions.
|
||||
* \throws Throws a ConversionException if the conversion the specified character set fails.
|
||||
* \remarks Strips the BOM of the specified \a text.
|
||||
*/
|
||||
inline TagValue::TagValue(std::string_view text, TagTextEncoding textEncoding, TagTextEncoding convertTo)
|
||||
: m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
assignText(text, textEncoding, convertTo);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding the given integer \a value.
|
||||
*/
|
||||
|
@ -356,56 +283,6 @@ inline TagParser::TagValue::TagValue(std::uint64_t value)
|
|||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue with a copy of the given \a data.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Strips the BOM of the specified \a data if \a type is TagDataType::Text.
|
||||
*/
|
||||
inline TagValue::TagValue(const char *data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
if (type == TagDataType::Text) {
|
||||
stripBom(data, m_size, encoding);
|
||||
}
|
||||
m_ptr = std::make_unique<char[]>(m_size);
|
||||
std::copy(data, data + m_size, m_ptr.get());
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding with the given \a data.
|
||||
*
|
||||
* The \a data is not copied. It is moved.
|
||||
*
|
||||
* \param data Specifies a pointer to the data.
|
||||
* \param length Specifies the length of the data.
|
||||
* \param type Specifies the type of the data as TagDataType.
|
||||
* \param encoding Specifies the encoding of the data as TagTextEncoding. The
|
||||
* encoding will only be considered if a text is assigned.
|
||||
* \remarks Does not strip the BOM so for consistency the caller must ensure there is no BOM present.
|
||||
*/
|
||||
inline TagValue::TagValue(std::unique_ptr<char[]> &&data, std::size_t length, TagDataType type, TagTextEncoding encoding)
|
||||
: m_size(length)
|
||||
, m_type(type)
|
||||
, m_encoding(encoding)
|
||||
, m_descEncoding(TagTextEncoding::Latin1)
|
||||
, m_flags(TagValueFlags::None)
|
||||
{
|
||||
if (length) {
|
||||
m_ptr = move(data);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given PositionInSet \a value.
|
||||
*/
|
||||
|
@ -422,6 +299,14 @@ inline TagValue::TagValue(CppUtilities::DateTime value)
|
|||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given DateTimeExpression \a value.
|
||||
*/
|
||||
inline TagValue::TagValue(const CppUtilities::DateTimeExpression &value)
|
||||
: TagValue(reinterpret_cast<const char *>(&value), sizeof(value), TagDataType::DateTimeExpression)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new TagValue holding a copy of the given TimeSpan \a value.
|
||||
*/
|
||||
|
@ -523,6 +408,14 @@ inline void TagValue::assignDateTime(CppUtilities::DateTime value)
|
|||
assignData(reinterpret_cast<const char *>(&value), sizeof(value), TagDataType::DateTime);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Assigns the given DateTimeExpression \a value.
|
||||
*/
|
||||
inline void TagParser::TagValue::assignDateTimeExpression(const CppUtilities::DateTimeExpression &value)
|
||||
{
|
||||
assignData(reinterpret_cast<const char *>(&value), sizeof(value), TagDataType::DateTimeExpression);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Assigns the given standard genre \a index to be assigned.
|
||||
* \param index Specifies the index to be assigned.
|
||||
|
|
|
@ -50,6 +50,7 @@ class OverallTests : public TestFixture {
|
|||
CPPUNIT_TEST(testFlacMaking);
|
||||
CPPUNIT_TEST(testMkvMakingWithDifferentSettings);
|
||||
CPPUNIT_TEST(testMkvMakingNestedTags);
|
||||
CPPUNIT_TEST(testVorbisCommentFieldHandling);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
|
@ -122,6 +123,7 @@ public:
|
|||
void testMp3Making();
|
||||
void testOggMaking();
|
||||
void testFlacMaking();
|
||||
void testVorbisCommentFieldHandling();
|
||||
|
||||
private:
|
||||
MediaFileInfo m_fileInfo;
|
||||
|
|
|
@ -35,7 +35,7 @@ void OverallTests::checkFlacTestfile1()
|
|||
CPPUNIT_ASSERT_EQUAL("1998"s, tags.front()->value(KnownField::RecordDate).toString());
|
||||
CPPUNIT_ASSERT(tags.front()->value(KnownField::Comment).isEmpty());
|
||||
//CPPUNIT_ASSERT(tags.front()->value(KnownField::Cover).dataSize() == 0x58f3);
|
||||
//CPPUNIT_ASSERT(BE::toUInt64(tags.front()->value(KnownField::Cover).dataPointer()) == 0xFFD8FFE000104A46);
|
||||
//CPPUNIT_ASSERT(BE::toInt<std::uint64_t>(tags.front()->value(KnownField::Cover).dataPointer()) == 0xFFD8FFE000104A46);
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(3, 4), tags.front()->value(KnownField::TrackPosition).toPositionInSet());
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(1, 1), tags.front()->value(KnownField::DiskPosition).toPositionInSet());
|
||||
break;
|
||||
|
|
|
@ -37,7 +37,7 @@ enum TestFlag {
|
|||
void OverallTests::checkMkvTestfile1()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1) + TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(336), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1.0) + TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(336.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -86,7 +86,7 @@ void OverallTests::checkMkvTestfile1()
|
|||
void OverallTests::checkMkvTestfile2()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47) + TimeSpan::fromMilliseconds(509), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47.0) + TimeSpan::fromMilliseconds(509.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -135,7 +135,7 @@ void OverallTests::checkMkvTestfile2()
|
|||
void OverallTests::checkMkvTestfile3()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(49) + TimeSpan::fromMilliseconds(64), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(49.0) + TimeSpan::fromMilliseconds(64.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -244,7 +244,7 @@ void OverallTests::checkMkvTestfile4()
|
|||
void OverallTests::checkMkvTestfile5()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(46) + TimeSpan::fromMilliseconds(665), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(46.0) + TimeSpan::fromMilliseconds(665.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(11_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -298,7 +298,7 @@ void OverallTests::checkMkvTestfile5()
|
|||
void OverallTests::checkMkvTestfile6()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1) + TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(336), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(1.0) + TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(336.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -348,7 +348,7 @@ void OverallTests::checkMkvTestfile6()
|
|||
void OverallTests::checkMkvTestfile7()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(37) + TimeSpan::fromMilliseconds(43), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(37.0) + TimeSpan::fromMilliseconds(43.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -408,7 +408,7 @@ void OverallTests::checkMkvTestfile7()
|
|||
void OverallTests::checkMkvTestfile8()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47) + TimeSpan::fromMilliseconds(341), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(47.0) + TimeSpan::fromMilliseconds(341.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
@ -459,7 +459,7 @@ void OverallTests::checkMkvTestfile8()
|
|||
void OverallTests::checkMkvTestfileHandbrakeChapters()
|
||||
{
|
||||
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Matroska, m_fileInfo.containerFormat());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(27) + TimeSpan::fromMilliseconds(569), m_fileInfo.duration());
|
||||
CPPUNIT_ASSERT_EQUAL(TimeSpan::fromSeconds(27.0) + TimeSpan::fromMilliseconds(569.0), m_fileInfo.duration());
|
||||
const auto tracks = m_fileInfo.tracks();
|
||||
CPPUNIT_ASSERT_EQUAL(2_st, tracks.size());
|
||||
for (const auto &track : tracks) {
|
||||
|
|
|
@ -209,7 +209,7 @@ void OverallTests::checkMp4Testfile4()
|
|||
CPPUNIT_ASSERT_EQUAL("1998"s, tags.front()->value(KnownField::RecordDate).toString());
|
||||
CPPUNIT_ASSERT(tags.front()->value(KnownField::Comment).isEmpty());
|
||||
CPPUNIT_ASSERT_EQUAL(0x58f3_st, tags.front()->value(KnownField::Cover).dataSize());
|
||||
CPPUNIT_ASSERT_EQUAL(0xFFD8FFE000104A46ul, BE::toUInt64(tags.front()->value(KnownField::Cover).dataPointer()));
|
||||
CPPUNIT_ASSERT_EQUAL(0xFFD8FFE000104A46ul, BE::toInt<std::uint64_t>(tags.front()->value(KnownField::Cover).dataPointer()));
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(3, 4), tags.front()->value(KnownField::TrackPosition).toPositionInSet());
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(1, 1), tags.front()->value(KnownField::DiskPosition).toPositionInSet());
|
||||
break;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include "../abstracttrack.h"
|
||||
#include "../tag.h"
|
||||
#include "../vorbis/vorbiscomment.h"
|
||||
#include "../vorbis/vorbiscommentfield.h"
|
||||
#include "../vorbis/vorbiscommentids.h"
|
||||
|
||||
#include <c++utilities/io/misc.h>
|
||||
|
||||
|
@ -250,3 +252,52 @@ void OverallTests::testOggMaking()
|
|||
makeFile(workingCopyPath("ogg/noise-without-cover.opus"), modifyRoutineCover, &OverallTests::checkOggTestfile3);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests the Vorbis Comment specifc handling of certain fields done in VorbisComment::convertTotalFields().
|
||||
*/
|
||||
void OverallTests::testVorbisCommentFieldHandling()
|
||||
{
|
||||
const auto context = std::string();
|
||||
const auto trackNumberFieldId = std::string(VorbisCommentIds::trackNumber());
|
||||
const auto trackTotalFieldId = std::string(VorbisCommentIds::trackTotal());
|
||||
const auto diskNumberFieldId = std::string(VorbisCommentIds::diskNumber());
|
||||
const auto diskTotalFieldId = std::string(VorbisCommentIds::diskTotal());
|
||||
|
||||
auto diag = Diagnostics();
|
||||
auto vc = VorbisComment();
|
||||
auto trackNumber = VorbisCommentField(trackNumberFieldId, TagValue(5));
|
||||
auto trackTotal = VorbisCommentField(trackTotalFieldId, TagValue(20));
|
||||
auto &fields = vc.fields();
|
||||
fields.insert(std::make_pair(trackNumberFieldId, std::move(trackNumber)));
|
||||
fields.insert(std::make_pair(trackTotalFieldId, std::move(trackTotal)));
|
||||
vc.convertTotalFields(context, diag);
|
||||
|
||||
const auto convertedValues = vc.values(trackNumberFieldId);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("the two fileds have been combined into one", 1_st, fields.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("there is exactly one track number value", 1_st, convertedValues.size());
|
||||
const auto convertedTrackNumber = convertedValues.front()->toPositionInSet();
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(5, 20), convertedTrackNumber);
|
||||
CPPUNIT_ASSERT_EQUAL(0_st, diag.size());
|
||||
|
||||
auto diskNumber = VorbisCommentField(diskNumberFieldId, TagValue("invalid pos"));
|
||||
auto diskTotal = VorbisCommentField(diskTotalFieldId, TagValue("invalid total"));
|
||||
auto diskTotal2 = VorbisCommentField(diskTotalFieldId, TagValue(42));
|
||||
fields.insert(std::make_pair(diskNumberFieldId, std::move(diskNumber)));
|
||||
fields.insert(std::make_pair(diskTotalFieldId, std::move(diskTotal)));
|
||||
fields.insert(std::make_pair(diskTotalFieldId, std::move(diskTotal2)));
|
||||
vc.convertTotalFields(context, diag);
|
||||
|
||||
const auto newDiskNumberValues = vc.values(diskNumberFieldId);
|
||||
const auto newDiskTotalValues = vc.values(diskTotalFieldId);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid fields have not been combined", 4_st, fields.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid disk position has been preserved and valid disk total converted", 2_st, newDiskNumberValues.size());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("invalid disk total has been preserved", 1_st, newDiskTotalValues.size());
|
||||
const auto preservedDiskNumber = newDiskNumberValues[0]->toString();
|
||||
const auto convertedDiskTotal = newDiskNumberValues[1]->toPositionInSet();
|
||||
const auto preservedDiskTotal = newDiskTotalValues[0]->toString();
|
||||
CPPUNIT_ASSERT_EQUAL("invalid pos"s, preservedDiskNumber);
|
||||
CPPUNIT_ASSERT_EQUAL(PositionInSet(0, 42), convertedDiskTotal);
|
||||
CPPUNIT_ASSERT_EQUAL("invalid total"s, preservedDiskTotal);
|
||||
CPPUNIT_ASSERT_EQUAL(3_st, diag.size());
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ class TagValueTests : public TestFixture {
|
|||
CPPUNIT_TEST(testPositionInSet);
|
||||
CPPUNIT_TEST(testTimeSpan);
|
||||
CPPUNIT_TEST(testDateTime);
|
||||
CPPUNIT_TEST(testDateTimeExpression);
|
||||
CPPUNIT_TEST(testPopularity);
|
||||
CPPUNIT_TEST(testString);
|
||||
CPPUNIT_TEST(testEqualityOperator);
|
||||
|
@ -45,6 +46,7 @@ public:
|
|||
void testPositionInSet();
|
||||
void testTimeSpan();
|
||||
void testDateTime();
|
||||
void testDateTimeExpression();
|
||||
void testPopularity();
|
||||
void testString();
|
||||
void testEqualityOperator();
|
||||
|
@ -155,7 +157,7 @@ void TagValueTests::testPositionInSet()
|
|||
|
||||
void TagValueTests::testTimeSpan()
|
||||
{
|
||||
const TimeSpan fiveMinutes(TimeSpan::fromMinutes(5));
|
||||
const TimeSpan fiveMinutes(TimeSpan::fromMinutes(5.0));
|
||||
TagValue timeSpan;
|
||||
timeSpan.assignTimeSpan(fiveMinutes);
|
||||
CPPUNIT_ASSERT_EQUAL(timeSpan, TagValue(timeSpan));
|
||||
|
@ -168,15 +170,29 @@ void TagValueTests::testTimeSpan()
|
|||
|
||||
void TagValueTests::testDateTime()
|
||||
{
|
||||
const DateTime now(DateTime::now());
|
||||
TagValue dateTime;
|
||||
dateTime.assignDateTime(now);
|
||||
CPPUNIT_ASSERT_EQUAL(dateTime, TagValue(dateTime));
|
||||
CPPUNIT_ASSERT_EQUAL(now, dateTime.toDateTime());
|
||||
CPPUNIT_ASSERT_EQUAL(now.toString(DateTimeOutputFormat::IsoOmittingDefaultComponents), dateTime.toString());
|
||||
CPPUNIT_ASSERT_THROW(dateTime.toInteger(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(dateTime.toTimeSpan(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(dateTime.toPositionInSet(), ConversionException);
|
||||
const auto now = DateTime::now();
|
||||
auto value = TagValue();
|
||||
value.assignDateTime(now);
|
||||
CPPUNIT_ASSERT_EQUAL(value, TagValue(value));
|
||||
CPPUNIT_ASSERT_EQUAL(now, value.toDateTime());
|
||||
CPPUNIT_ASSERT_EQUAL(now.toIsoString(), value.toString());
|
||||
CPPUNIT_ASSERT_THROW(value.toInteger(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(value.toTimeSpan(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(value.toPositionInSet(), ConversionException);
|
||||
}
|
||||
|
||||
void TagValueTests::testDateTimeExpression()
|
||||
{
|
||||
auto expr = DateTimeExpression::fromIsoString("2007");
|
||||
auto value = TagValue();
|
||||
value.assignDateTimeExpression(expr);
|
||||
CPPUNIT_ASSERT_EQUAL(value, TagValue(expr));
|
||||
CPPUNIT_ASSERT_EQUAL(expr.value, value.toDateTime());
|
||||
CPPUNIT_ASSERT_EQUAL(expr, value.toDateTimeExpression());
|
||||
CPPUNIT_ASSERT_EQUAL(expr.toIsoString(), value.toString());
|
||||
CPPUNIT_ASSERT_THROW(value.toInteger(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(value.toTimeSpan(), ConversionException);
|
||||
CPPUNIT_ASSERT_THROW(value.toPositionInSet(), ConversionException);
|
||||
}
|
||||
|
||||
void TagValueTests::testPopularity()
|
||||
|
@ -228,7 +244,9 @@ void TagValueTests::testString()
|
|||
"conversion to pos", PositionInSet(15), TagValue("\0\x31\0\x35", 4, TagTextEncoding::Utf16BigEndian).toPositionInSet());
|
||||
CPPUNIT_ASSERT_THROW_MESSAGE("failing conversion pos", TagValue("a4 / 15", 7, TagTextEncoding::Utf8).toPositionInSet(), ConversionException);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE(
|
||||
"conversion to date", DateTime::fromDate(2004, 4, 15), TagValue("2004-04-15", 10, TagTextEncoding::Utf8).toDateTime());
|
||||
"conversion to date time", DateTime::fromDate(2004, 4, 15), TagValue("2004-04-15", 10, TagTextEncoding::Utf8).toDateTime());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to date time expression", DateTimeExpression::fromIsoString("2004-04"),
|
||||
TagValue("2004-04-15", 7, TagTextEncoding::Utf8).toDateTimeExpression());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("conversion to date from UTF-16", DateTime::fromDate(2015, 4, 15),
|
||||
TagValue("\0\x32\0\x30\0\x31\0\x35\0\x2d\0\x30\0\x34\0\x2d\0\x31\0\x35", 20, TagTextEncoding::Utf16BigEndian).toDateTime());
|
||||
CPPUNIT_ASSERT_THROW_MESSAGE("failing conversion to date", TagValue("_", 1, TagTextEncoding::Utf8).toDateTime(), ConversionException);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <fstream>
|
||||
|
@ -81,14 +82,35 @@ struct TestFile {
|
|||
{ "ogg/example-cover.png", { "897e1a2d0cfb79c1fe5068108bb34610c3758bd0b9a7e90c1702c4e6972e0801" } },
|
||||
};
|
||||
|
||||
/// \cond
|
||||
struct EvpMdCtx {
|
||||
EvpMdCtx()
|
||||
: handle(EVP_MD_CTX_new())
|
||||
{
|
||||
}
|
||||
~EvpMdCtx()
|
||||
{
|
||||
if (handle) {
|
||||
EVP_MD_CTX_free(handle);
|
||||
}
|
||||
}
|
||||
EVP_MD_CTX *handle;
|
||||
};
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Computes the SHA-256 checksums for the file using OpenSSL.
|
||||
*/
|
||||
Sha256Checksum TestFile::computeSha256Sum() const
|
||||
{
|
||||
// init sha256 hashing
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
const auto mdctx = EvpMdCtx();
|
||||
if (!mdctx.handle) {
|
||||
throw std::runtime_error("Unable to create EVP context.");
|
||||
}
|
||||
if (EVP_DigestInit_ex(mdctx.handle, EVP_sha256(), nullptr) != 1) {
|
||||
throw std::runtime_error("Unable to init SHA256-EVP context.");
|
||||
}
|
||||
|
||||
// read and hash file
|
||||
{
|
||||
|
@ -100,11 +122,15 @@ Sha256Checksum TestFile::computeSha256Sum() const
|
|||
try {
|
||||
for (;;) {
|
||||
file.read(readBuffer, sizeof(readBuffer));
|
||||
SHA256_Update(&sha256, readBuffer, static_cast<size_t>(file.gcount()));
|
||||
if (EVP_DigestUpdate(mdctx.handle, readBuffer, static_cast<std::size_t>(file.gcount())) != 1) {
|
||||
throw std::runtime_error("Unable to update SHA256-EVP.");
|
||||
}
|
||||
}
|
||||
} catch (const std::ios_base::failure &) {
|
||||
if (file.eof() && !file.bad()) {
|
||||
SHA256_Update(&sha256, readBuffer, static_cast<size_t>(file.gcount()));
|
||||
if (EVP_DigestUpdate(mdctx.handle, readBuffer, static_cast<std::size_t>(file.gcount())) != 1) {
|
||||
throw std::runtime_error("Unable to update SHA256-EVP.");
|
||||
}
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
|
@ -113,7 +139,10 @@ Sha256Checksum TestFile::computeSha256Sum() const
|
|||
|
||||
// compute final hash
|
||||
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||
SHA256_Final(hash, &sha256);
|
||||
auto length = static_cast<unsigned int>(SHA256_DIGEST_LENGTH);
|
||||
if (EVP_DigestFinal_ex(mdctx.handle, hash, &length) != 1) {
|
||||
throw std::runtime_error("Unable to finalize SHA256-EVP.");
|
||||
}
|
||||
|
||||
// convert to "hex string"
|
||||
Sha256Checksum hexString;
|
||||
|
|
|
@ -21,10 +21,9 @@ using namespace CppUtilities;
|
|||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace CppUtilities::Literals;
|
||||
using namespace TagParser;
|
||||
|
@ -335,8 +334,7 @@ void UtilitiesTests::testBackupFile()
|
|||
// get rid of 2nd backup (again)
|
||||
backupStream2.close();
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("remove " + backupPath2, 0, remove(backupPath2.data()));
|
||||
const auto backupDir(workingDir + "/bak");
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("remove " + backupDir, 0, rmdir(backupDir.data()));
|
||||
std::filesystem::remove_all(workingDir + "/bak");
|
||||
|
||||
// should be able to use backup stream, eg. seek to the end
|
||||
backupStream1.seekg(0, ios_base::end);
|
||||
|
|
|
@ -105,6 +105,12 @@ VorbisComment::IdentifierType VorbisComment::internallyGetFieldId(KnownField fie
|
|||
return std::string(isrc());
|
||||
case KnownField::Rating:
|
||||
return std::string(rating());
|
||||
case KnownField::Bpm:
|
||||
return std::string(bpm());
|
||||
case KnownField::Publisher:
|
||||
return std::string(publisher());
|
||||
case KnownField::PublisherWebpage:
|
||||
return std::string(publisherWebpage());
|
||||
default:
|
||||
return std::string();
|
||||
}
|
||||
|
@ -143,12 +149,76 @@ KnownField VorbisComment::internallyGetKnownField(const IdentifierType &id) cons
|
|||
{ director(), KnownField::Director },
|
||||
{ isrc(), KnownField::ISRC },
|
||||
{ rating(), KnownField::Rating },
|
||||
{ bpm(), KnownField::Bpm },
|
||||
{ publisher(), KnownField::Publisher },
|
||||
{ publisherWebpage(), KnownField::PublisherWebpage },
|
||||
});
|
||||
// clang-format on
|
||||
const auto knownField(fieldMap.find(id));
|
||||
return knownField != fieldMap.cend() ? knownField->second : KnownField::Invalid;
|
||||
}
|
||||
|
||||
/// \cond
|
||||
void VorbisComment::extendPositionInSetField(std::string_view field, std::string_view totalField, const std::string &diagContext, Diagnostics &diag)
|
||||
{
|
||||
auto totalValues = std::vector<std::int32_t>();
|
||||
auto fieldsIter = fields().equal_range(std::string(totalField));
|
||||
auto fieldsDist = std::distance(fieldsIter.first, fieldsIter.second);
|
||||
if (!fieldsDist) {
|
||||
return;
|
||||
}
|
||||
totalValues.reserve(static_cast<std::size_t>(fieldsDist));
|
||||
for (; fieldsIter.first != fieldsIter.second;) {
|
||||
try {
|
||||
totalValues.emplace_back(fieldsIter.first->second.value().toInteger());
|
||||
fields().erase(fieldsIter.first++);
|
||||
} catch (const ConversionException &e) {
|
||||
diag.emplace_back(DiagLevel::Warning, argsToString("Unable to parse \"", totalField, "\" as integer: ", e.what()), diagContext);
|
||||
totalValues.emplace_back(0);
|
||||
++fieldsIter.first;
|
||||
}
|
||||
}
|
||||
|
||||
auto totalIter = totalValues.begin(), totalEnd = totalValues.end();
|
||||
for (fieldsIter = fields().equal_range(std::string(field)); fieldsIter.first != fieldsIter.second && totalIter != totalEnd;
|
||||
++fieldsIter.first, ++totalIter) {
|
||||
auto &v = fieldsIter.first->second.value();
|
||||
try {
|
||||
auto p = v.toPositionInSet();
|
||||
if (p.total() && p.total() != *totalIter) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("The \"", totalField, "\" field value (", *totalIter, ") does not match \"", field, "\" field value (", p.total(),
|
||||
"). Discarding the former in favor of the latter."),
|
||||
diagContext);
|
||||
} else {
|
||||
p.setTotal(*totalIter);
|
||||
v.assignPosition(p);
|
||||
}
|
||||
} catch (const ConversionException &e) {
|
||||
diag.emplace_back(DiagLevel::Warning,
|
||||
argsToString("Unable to parse \"", field, "\" as position in set for incorporating \"", totalField, "\": ", e.what()), diagContext);
|
||||
}
|
||||
}
|
||||
if (totalIter != totalEnd) {
|
||||
diag.emplace_back(
|
||||
DiagLevel::Warning, argsToString("Vorbis Comment contains more \"", totalField, "\" fields than \"", field, "\" fields."), diagContext);
|
||||
}
|
||||
for (; totalIter != totalEnd; ++totalIter) {
|
||||
fields().insert(std::make_pair(field, VorbisCommentField(std::string(field), TagValue(PositionInSet(0, *totalIter)))));
|
||||
}
|
||||
}
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Converts TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields instead.
|
||||
*/
|
||||
void VorbisComment::convertTotalFields(const std::string &diagContext, Diagnostics &diag)
|
||||
{
|
||||
extendPositionInSetField(VorbisCommentIds::trackNumber(), VorbisCommentIds::trackTotal(), diagContext, diag);
|
||||
extendPositionInSetField(VorbisCommentIds::diskNumber(), VorbisCommentIds::diskTotal(), diagContext, diag);
|
||||
extendPositionInSetField(VorbisCommentIds::partNumber(), VorbisCommentIds::partTotal(), diagContext, diag);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Internal implementation for parsing.
|
||||
*/
|
||||
|
@ -164,7 +234,7 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
if (!skipSignature) {
|
||||
CHECK_MAX_SIZE(7)
|
||||
stream.read(sig, 7);
|
||||
skipSignature = (BE::toUInt64(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
|
||||
skipSignature = (BE::toInt<std::uint64_t>(sig) & 0xffffffffffffff00u) == 0x03766F7262697300u;
|
||||
}
|
||||
if (skipSignature) {
|
||||
// read vendor (length prefixed string)
|
||||
|
@ -175,7 +245,7 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
if (vendorSize <= maxSize) {
|
||||
auto buff = make_unique<char[]>(vendorSize);
|
||||
stream.read(buff.get(), vendorSize);
|
||||
m_vendor.assignData(move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8);
|
||||
m_vendor.assignData(std::move(buff), vendorSize, TagDataType::Text, TagTextEncoding::Utf8);
|
||||
// TODO: Is the vendor string actually UTF-8 (like the field values)?
|
||||
} else {
|
||||
diag.emplace_back(DiagLevel::Critical, "Vendor information is truncated.", context);
|
||||
|
@ -192,7 +262,7 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
VorbisCommentField field;
|
||||
try {
|
||||
field.parse(stream, maxSize, diag);
|
||||
fields().emplace(field.id(), move(field));
|
||||
fields().emplace(field.id(), std::move(field));
|
||||
} catch (const TruncatedDataException &) {
|
||||
throw;
|
||||
} catch (const Failure &) {
|
||||
|
@ -240,6 +310,10 @@ template <class StreamType> void VorbisComment::internalParse(StreamType &stream
|
|||
diag.emplace_back(DiagLevel::Warning, argsToString(bytesRemaining, " bytes left in last segment."), context);
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & VorbisCommentFlags::ConvertTotalFields) {
|
||||
convertTotalFields(context, diag);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#include "../fieldbasedtag.h"
|
||||
#include "../mediaformat.h"
|
||||
|
||||
class OverallTests;
|
||||
|
||||
namespace TagParser {
|
||||
|
||||
class OggIterator;
|
||||
|
@ -24,6 +26,7 @@ public:
|
|||
|
||||
class TAG_PARSER_EXPORT VorbisComment : public FieldMapBasedTag<VorbisComment> {
|
||||
friend class FieldMapBasedTag<VorbisComment>;
|
||||
friend class ::OverallTests;
|
||||
|
||||
public:
|
||||
VorbisComment();
|
||||
|
@ -52,6 +55,8 @@ protected:
|
|||
|
||||
private:
|
||||
template <class StreamType> void internalParse(StreamType &stream, std::uint64_t maxSize, VorbisCommentFlags flags, Diagnostics &diag);
|
||||
void extendPositionInSetField(std::string_view field, std::string_view totalField, const std::string &diagContext, Diagnostics &diag);
|
||||
void convertTotalFields(const std::string &diagContext, Diagnostics &diag);
|
||||
|
||||
private:
|
||||
TagValue m_vendor;
|
||||
|
|
|
@ -78,7 +78,11 @@ template <class StreamType> void VorbisCommentField::internalParse(StreamType &s
|
|||
auto decoded = decodeBase64(data.get() + idSize + 1, size - idSize - 1);
|
||||
stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary);
|
||||
bufferStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
|
||||
bufferStream.rdbuf()->pubsetbuf(reinterpret_cast<char *>(decoded.first.get()), decoded.second);
|
||||
#else
|
||||
bufferStream.write(reinterpret_cast<const char *>(decoded.first.get()), decoded.second);
|
||||
#endif
|
||||
FlacMetaDataBlockPicture pictureBlock(value());
|
||||
pictureBlock.parse(bufferStream, decoded.second);
|
||||
setTypeInfo(pictureBlock.pictureType());
|
||||
|
@ -197,10 +201,15 @@ bool VorbisCommentField::make(BinaryWriter &writer, VorbisCommentFlags flags, Di
|
|||
auto buffer = make_unique<char[]>(requiredSize);
|
||||
stringstream bufferStream(ios_base::in | ios_base::out | ios_base::binary);
|
||||
bufferStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
|
||||
bufferStream.rdbuf()->pubsetbuf(buffer.get(), requiredSize);
|
||||
|
||||
#endif
|
||||
pictureBlock.make(bufferStream);
|
||||
#if defined(__GLIBCXX__) && !defined(_LIBCPP_VERSION)
|
||||
bufferStream.read(buffer.get(), static_cast<std::streamsize>(requiredSize));
|
||||
#endif
|
||||
valueString = encodeBase64(reinterpret_cast<std::uint8_t *>(buffer.get()), requiredSize);
|
||||
|
||||
} catch (const Failure &) {
|
||||
diag.emplace_back(DiagLevel::Critical, "Unable to make METADATA_BLOCK_PICTURE struct from the assigned value.", context);
|
||||
throw;
|
||||
|
|
|
@ -19,7 +19,8 @@ enum class VorbisCommentFlags : std::uint8_t {
|
|||
None = 0x0, /**< Regular parsing/making. */
|
||||
NoSignature = 0x1, /**< Skips the signature when parsing and making. */
|
||||
NoFramingByte = 0x2, /**< Doesn't expect the framing bit to be present when parsing; does not make the framing bit when making. */
|
||||
NoCovers = 0x4 /**< Skips all covers when making. */
|
||||
NoCovers = 0x4, /**< Skips all covers when making. */
|
||||
ConvertTotalFields = 0x8, /**< Converts TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields. */
|
||||
};
|
||||
|
||||
} // namespace TagParser
|
||||
|
|
|
@ -9,7 +9,12 @@ namespace TagParser {
|
|||
|
||||
/*!
|
||||
* \brief Encapsulates Vorbis comment field names.
|
||||
* \sa See https://xiph.org/vorbis/doc/v-comment.html for the upstream documentation of the field names.
|
||||
* \sa
|
||||
* - See https://xiph.org/vorbis/doc/v-comment.html for the upstream documentation of the field names.
|
||||
* - See https://wiki.xiph.org/Field_names for an additional proposal that is most notably introducing
|
||||
* `DISCNUMBER` and `…TOTAL` fields.
|
||||
* - See https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping for further conventions and a
|
||||
* comparision with other formats.
|
||||
*/
|
||||
namespace VorbisCommentIds {
|
||||
|
||||
|
@ -17,10 +22,18 @@ constexpr TAG_PARSER_EXPORT std::string_view trackNumber()
|
|||
{
|
||||
return "TRACKNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view trackTotal()
|
||||
{
|
||||
return "TRACKTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view diskNumber()
|
||||
{
|
||||
return "DISCNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view diskTotal()
|
||||
{
|
||||
return "DISCTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view part()
|
||||
{
|
||||
return "PART";
|
||||
|
@ -29,6 +42,10 @@ constexpr TAG_PARSER_EXPORT std::string_view partNumber()
|
|||
{
|
||||
return "PARTNUMBER";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view partTotal()
|
||||
{
|
||||
return "PARTTOTAL";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view title()
|
||||
{
|
||||
return "TITLE";
|
||||
|
@ -185,6 +202,14 @@ constexpr TAG_PARSER_EXPORT std::string_view cover()
|
|||
{
|
||||
return "METADATA_BLOCK_PICTURE";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view bpm()
|
||||
{
|
||||
return "BPM";
|
||||
}
|
||||
constexpr TAG_PARSER_EXPORT std::string_view publisherWebpage()
|
||||
{
|
||||
return "WWWPUBLISHER";
|
||||
}
|
||||
|
||||
} // namespace VorbisCommentIds
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ void VorbisIdentificationHeader::parseHeader(OggIterator &iterator)
|
|||
{
|
||||
char buff[30 - 7];
|
||||
iterator.read(buff, 7);
|
||||
if ((BE::toUInt64(buff) & 0xffffffffffffff00u) != 0x01766F7262697300u) {
|
||||
if ((BE::toInt<std::uint64_t>(buff) & 0xffffffffffffff00u) != 0x01766F7262697300u) {
|
||||
throw InvalidDataException(); // not Vorbis identification header
|
||||
}
|
||||
iterator.read(buff, sizeof(buff));
|
||||
|
|
Loading…
Reference in New Issue