Compare commits

...

90 Commits

Author SHA1 Message Date
Martchus acfb9ef219 Handle TRACKTOTAL/DISCTOTAL/PARTTOTAL fields in Vorbis Comments
* Move those fields into their corresponding
  TRACKNUMBER/DISCNUMBER/PARTNUMBER fields after parsing so they are
  accessible via just one field as PositionInSet which is in line with
  other tag formats and also how other software like VLC expect the total
  to be specified
* NOT implemented yet: Move those fields optionally back into separate
  fields when serializing
2024-02-28 21:36:06 +01:00
Martchus 351e953b83 Avoid empty documentation entry for `EvpMdCtx` 2024-02-28 21:36:06 +01:00
Martchus 8204b2dfde Apply change of `global.h` template 2024-02-27 01:57:17 +01:00
Martchus 8246d30ec5 Update `global.h` via updated template in c++utilities 2024-01-30 23:32:35 +01:00
Martchus d48722f26c Fix unity builds
* Define `CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS` consistently
  with all necassary changes
* Avoid ambiguity between enum members and certain class/struct names
2024-01-30 23:31:15 +01:00
Martchus 45ab6b17b9 Improve various details of documentation 2024-01-23 00:29:11 +01:00
Martchus 6fb34ec3b3 Update copyright date 2024-01-23 00:28:19 +01:00
Martchus 33327390e9 Add Vorbis comment IDs for total number of tracks/disks/parts 2023-12-29 17:12:20 +01:00
Martchus 7f3d4c5751 Add ID3v2 frame definitions for publisher web page and user defined URLs 2023-12-29 17:07:14 +01:00
Martchus b1bca85ef4 Support publisher webpage in Vorbis comments 2023-12-29 16:50:41 +01:00
Martchus 0f669c88a7 Bump minor version 2023-12-29 16:50:19 +01:00
Martchus 1df871870b Map the publisher field for Vorbis comments 2023-12-29 15:57:51 +01:00
Martchus 909a3ee98a Improve comments in Matroska container code 2023-10-31 21:27:17 +01:00
Martchus 5aef3f84ee Bump patch version 2023-10-31 20:07:02 +01:00
Martchus 56ccd3da80 Use auto-syntax in places touched by previous commit consistently 2023-10-31 20:06:38 +01:00
Martchus 9f41c30443 Silence/fix GCC's maybe-uninitialized warnings
* The warning about `bsEnvCount` is actually correct.
* The warning about `lastAtomToBeWritten` might be correct.
* The warning about `relPos` is definitely unjustified because `relPos` is
  only used when `cueRelativePositionElement` is not `nullptr` and `relPos`
  is initialized in that case.
* The warnings about `pos`, `nextPageOffset` and `startOfLastMetaDataBlock`
  are also wrong for similar reasons.
2023-10-31 20:05:13 +01:00
Martchus ef0ab3d8c3 Avoid GCC's stringop-truncation warning
Not copying the termination character here is wanted. Just use
`std::memcpy` to avoid it as the special behavior of `std::strncpy` is not
needed here anyways.
2023-10-31 19:36:12 +01:00
Martchus 0827002183 Fix duration and bitrate calculation of MP3 files via XING header
* Calculate the duration independently of the bitrate which is supposedly
  more accurate
* Fix conversion factor for computing bitrate
* Use real size if it differs from size specified in Xing header; this way
  the bitrate is consistent with MediaInfo and other tools (there is
  possibly still a bug in the way the size is read from the Xing header,
  though)
2023-08-21 12:09:07 +02:00
Martchus f2e97b9899 Fix skipping TOC field of Xing header 2023-08-19 01:09:22 +02:00
Martchus db5e1f2c8c Fix check for presence of frame field in Xing header 2023-08-19 00:44:43 +02:00
Martchus 04795f957f Fix setting writing app name after allowing to preserve it 2023-08-09 23:26:34 +02:00
Martchus c0e9f9bf83 Fix condition for preserving muxing/writing app 2023-08-09 23:17:26 +02:00
Martchus e6bb98d6e6 Improve dealing with muxing/writing application of Matroska files
* Allow reading the current muxing/writing application
* Allow to preserve the original muxing/writing application instead of
  always overriding
2023-08-08 17:18:02 +02:00
Martchus f5497fb300 Allow setting position/total of PositionInSet 2023-08-01 00:24:28 +02:00
Martchus f7941d442f Fix typo 2023-07-30 14:40:15 +02:00
Martchus 03f9698269 Expose tagDataTypeString() 2023-07-29 16:02:14 +02:00
Martchus 8a6cffad95 Bump minor version 2023-07-29 16:02:14 +02:00
Martchus 54a87cd32c Avoid CMake deprecation warning by bumping version 2023-07-23 20:59:19 +02:00
Martchus 92345027fb Use generic `toInt()` function which relies less on compiler optimizations 2023-05-18 00:52:28 +02:00
Martchus 90cace1e95 Fix computation of effective size when APE tag is present at end of file 2023-05-18 00:41:35 +02:00
Martchus 6eab8b8718 Log the mismatching sizes when XING header mismatches real size 2023-05-18 00:37:59 +02:00
Martchus a5ab3ed1b2 Avoid abusing OggPage header flags to store whether last segment unconcluded 2023-05-16 23:18:27 +02:00
Martchus 5745632af7 Move `maxFullParseSize()` to `MediaFileInfo` as non-static member 2023-05-16 23:11:53 +02:00
Martchus 6f321b7b00 Fix typo in ContainerFormat enum 2023-05-16 23:00:32 +02:00
Martchus 60385aa347 Generalize copy functions in GenericFileElement to eventually use sendfile64()
Since we're still using `container().stream()` in `copyInternal()` this change
is still not really effective.
2023-05-16 22:58:04 +02:00
Martchus c5cd20682d Determine "effective size" via file info instead of track implementations
This allows removing duplicated code from the track implementations to take
APE tags into account.
2023-05-16 22:39:26 +02:00
Martchus 8ad28f857b Allow extending important classes without ABI break
This allows to make ABI breaks less often while still
being able to extend many aspects of the library.
2023-05-16 22:20:48 +02:00
Martchus 6ed968f5e6 Update major version to 12 2023-05-16 22:00:35 +02:00
Martchus a167e0702e Ignore iso_639-2.json 2023-05-11 22:59:09 +02:00
Martchus 405631625f Fix typo 2023-05-08 11:15:53 +02:00
Martchus 6fb16d72eb Avoid unqualified call to `std::strncpy()` 2023-05-07 21:45:47 +02:00
Martchus 111d6190cb Avoid some of the warnings from MSVC 2023-05-07 21:45:15 +02:00
Martchus 0a2b948f26 Detect APE tags, emit according messages and update README
It is likely not worth adding support for APE tags at this point.
However, it still makes sense to acknowledge the presence of them
despite not being actually supported to avoid possible confusion.

With this change, APE tags at the beginning of the file will be
dropped when applying changes as the container offset is updated
when skipping the tag. I suppose that makes sense considering
putting those tags at the beginning is not recommended anyways.
The diag messages and README have been updated accordingly.
2023-05-06 13:04:01 +02:00
Martchus 9e4d221bb1 Bump minor version 2023-05-06 12:51:16 +02:00
Martchus ea6b474e8c Add container format name for ID3v2 2023-05-06 11:12:08 +02:00
Martchus 10a6b10658 Fix typos 2023-05-06 11:07:31 +02:00
Martchus 25166e4cb4 Bump patch version 2023-05-06 11:00:52 +02:00
Martchus 7d9d5c8408 Detect zstd compressed files 2023-03-05 22:37:14 +01:00
Martchus 21edf75047 Ease setting up build by downloading iso_639-2.json automatically 2023-03-04 19:00:24 +01:00
Martchus d80736743b Use `pubsetbuf` only with `libstdc++`
This usage of the function seems only to work as intended with that
standard lib. With `libc++` and the MSVC standard lib the call has no
effect.
2023-02-28 21:06:52 +01:00
Martchus 981db492e4 Avoid unqualified calls to `std::move` 2023-02-20 19:54:42 +01:00
Martchus 15d74bbfb0 Add media format entry for H.266 2023-02-18 21:43:16 +01:00
Martchus 3272d9c511 Bump minor version 2023-02-18 21:42:38 +01:00
Martchus 522aa4359e Avoid use of platform-specific `unistd.h` header 2023-02-11 13:55:04 +01:00
Martchus cb93d6900c Apply clang-format 2023-02-03 13:31:40 +01:00
Martchus a9c4bca679 Fix broken doc comment 2023-02-03 12:45:44 +01:00
Martchus ca6abe31a0 Fix compilation with MSVC
Apparently the move c'tor isn't available unless explicitly specified
leading to errors when `SegmentData` is used in `std::vector`.
2023-02-02 00:28:01 +01:00
Martchus 205b119416 Use `std` more consistently in `matroskacontainer.cpp` 2023-02-02 00:24:49 +01:00
Martchus 338011e3eb Add missing headers to `matroskaseekinfo.h` 2023-02-02 00:21:37 +01:00
Martchus 7873db611a Add copy c'tor for `AbortableProgressFeedback` to fix compilation with MSVC 2023-02-01 14:36:11 +01:00
Martchus dc4e4082e0 Use `std::filesystem::resize_file` instead of POSIX function
* Avoid platform-specific code
* Fix build with MSVC
2023-02-01 14:05:32 +01:00
Martchus 937631e5c4 Update copyright notice 2023-01-17 18:33:49 +01:00
Martchus 6a07d60649 Avoid message "Parsing tags not implemented …" for MPEG audio frames
This info message that is generated for e.g. MP3 files without ID3 tags is
misleading.
2023-01-08 21:11:57 +01:00
Martchus edc7aa06c9 Bump patch version 2023-01-08 21:10:04 +01:00
Martchus 04e6996ce3 Use the output file path when making an MP4 file also if no rewrite required
Otherwise the `--output-files` option of the tag editor will be ignored
when editing an MP4 file and no rewrite is required.

Note that the condition is already this way in `MediaFileInfo::makeMp3File()`
and `MatroskaContainer::internalMakeFile()`.
2023-01-02 20:04:42 +01:00
Martchus 766e7657b5 Preserve explicitly "undefined" language of MP4 tracks 2023-01-02 19:50:30 +01:00
Martchus 8d4c315611 Improve error messages about missing mandatory MP4 atoms 2023-01-02 19:18:07 +01:00
Martchus 3193df8e3f Fix reading duration from MP4's track header atom
* Skip 4 bytes reserved space correctly
* See https://github.com/Martchus/tageditor/issues/98
2023-01-02 19:16:45 +01:00
Martchus 0347ca73eb Avoid using deprecated OpenSSL functions in testsuite 2023-01-02 01:56:58 +01:00
Martchus dbdfd015bb Bump patch version 2023-01-02 01:55:53 +01:00
Martchus 93da1f1e25 Create ID3v2 tag for AIFF files within `createAppropriateTags()`
Wikipedia says an ID3v2 tag can be added and other software seems to be
able to cope with it.
2022-08-17 23:11:20 +02:00
Martchus bfe6ce8c1e Support detecting AIFF format 2022-08-17 23:06:09 +02:00
Martchus 49c6b61e0c Streamline coding style of date time related tests 2022-08-13 16:05:39 +02:00
Martchus 6333aaa84b Adapt DateTime test to latest changes 2022-08-13 16:04:56 +02:00
Martchus 4ac97910e9 Revert "Adapt tests to recent changes" partially
* In accordance with revert of 44fea6c8c2.
* This partially reverts commit 965ee4ab4b.
2022-08-13 15:43:28 +02:00
Martchus 0cc9271a7f Revert "Omit default components when formatting date time"
* This hack is no longer required as it is now possible to preserve
  what date time parts are specified.
* This reverts commit 44fea6c8c2.
2022-08-13 15:43:28 +02:00
Martchus b50de3cf4f Preserve present parts when converting old record date fields 2022-08-13 15:43:28 +02:00
Martchus f0d8a6efa7 Set only present parts when setting recording time of ID3 <= v2.3
See https://github.com/Martchus/tageditor/issues/86
2022-08-13 14:47:49 +02:00
Martchus 74f6d2b6ac Fix reference to ID3v2 field in error message 2022-08-13 14:43:43 +02:00
Martchus 4aff37b788 Support `CppUtilities::DateTimeExpression` in `TagValue` 2022-08-13 14:42:51 +02:00
Martchus d6a2903749 Fix namespace in documentation of TagDataType 2022-08-13 12:30:59 +02:00
Martchus 99bb786eeb Improve comments about `lRecordingTime`-mapping 2022-08-12 19:41:15 +02:00
Martchus 6da62db035 Add mapping for BPM in Vorbis Comments 2022-08-12 00:37:25 +02:00
Martchus 63c76e6ca4 Bump minor version 2022-08-12 00:36:59 +02:00
Martchus f068c44172 Prevent adding fields with an invalid ID
When there's no mapping for the specified known field, `fieldId()` returns
an invalid field ID. It should not be passed to `setValues()` as we would
otherwise attempt to create an invalid field.
2022-08-12 00:32:17 +02:00
Martchus df27013c2f Avoid misleading log message
"Writing frames" should just be "Writing data" because whatever the file
contains might not be (some kind of) frames.
2022-08-12 00:07:02 +02:00
Martchus 5dd5e301b3 Fix adding ID3v2 tag to file with unknown container format
The bytes skipped while searching for MP3 frames should not be used as
container offset. The container offset should just the offset after the
first ID3v2 tag (or zero if there are no tags).
2022-08-12 00:02:38 +02:00
Martchus 2484cb03f8 Fix merging ID3v2 tags 2022-08-11 23:59:40 +02:00
Martchus 60233c2af2 Add missing documentation 2022-08-02 21:41:05 +02:00
Martchus b4d9a3aeb8 Bump patch version 2022-08-02 21:40:24 +02:00
77 changed files with 1124 additions and 448 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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()

View File

@ -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).

View File

@ -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;

View File

@ -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;
}

View File

@ -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.
*/

View File

@ -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.

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};
/*!

View File

@ -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

View File

@ -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.
*/

View File

@ -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());

View File

@ -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.")

View File

@ -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);
}

View File

@ -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

View File

@ -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();
}

View File

@ -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 &) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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);

View File

@ -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");
}

View File

@ -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;
}

View File

@ -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

View File

@ -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 &) {

View File

@ -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
*/

View File

@ -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);
}
/*!

View File

@ -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>

View File

@ -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);

View File

@ -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.
*/

View File

@ -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()) {

View File

@ -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:

View File

@ -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,

View File

@ -3,7 +3,9 @@
#include "./ebmlelement.h"
#include <memory>
#include <utility>
#include <vector>
namespace TagParser {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 */
};
/*!

View File

@ -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);

View File

@ -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 &) {
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}

View File

@ -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:

View File

@ -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();

View File

@ -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);
}
/*!

View File

@ -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

View File

@ -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 &params = 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;

View File

@ -12,9 +12,11 @@
#include <tuple>
#include <unordered_map>
/// \cond
namespace CppUtilities {
template <std::size_t bufferSize> class CopyHelper;
}
/// \endcond
namespace TagParser {

View File

@ -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

View File

@ -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;
}
/*!

View File

@ -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));
}

View File

@ -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.
*/

View File

@ -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().
*/

View File

@ -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 "";
}

View File

@ -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);

View File

@ -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
View File

@ -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;
};

View File

@ -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));

View File

@ -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;
}

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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());
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}
/*!

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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));