Tag Parser 12.3.1
C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags
Loading...
Searching...
No Matches
mediafileinfo.cpp
Go to the documentation of this file.
1#include "./mediafileinfo.h"
2#include "./abstracttrack.h"
3#include "./backuphelper.h"
4#include "./diagnostics.h"
5#include "./exceptions.h"
6#include "./locale.h"
8#include "./signature.h"
9#include "./tag.h"
10
11#include "./id3/id3v1tag.h"
12#include "./id3/id3v2tag.h"
13
15
17
18#include "./adts/adtsstream.h"
19
20#include "./ivf/ivfstream.h"
21
22#include "./mp4/mp4atom.h"
23#include "./mp4/mp4container.h"
24#include "./mp4/mp4ids.h"
25#include "./mp4/mp4tag.h"
26#include "./mp4/mp4track.h"
27
32
33#include "./ogg/oggcontainer.h"
34
35#include "./flac/flacmetadata.h"
36#include "./flac/flacstream.h"
37
38#include <c++utilities/chrono/timespan.h>
39#include <c++utilities/conversion/stringconversion.h>
40#include <c++utilities/io/path.h>
41
42#include <algorithm>
43#include <cstdint>
44#include <cstdio>
45#include <filesystem>
46#include <functional>
47#include <iomanip>
48#include <ios>
49#include <memory>
50#include <system_error>
51
52using namespace std;
53using namespace std::placeholders;
54using namespace CppUtilities;
55
61namespace TagParser {
62
65
82 : BasicFileInfo(std::move(path))
83 , m_containerParsingStatus(ParsingStatus::NotParsedYet)
84 , m_containerFormat(ContainerFormat::Unknown)
85 , m_containerOffset(0)
86 , m_paddingSize(0)
87 , m_effectiveSize(0)
88 , m_fileStructureFlags(MediaFileStructureFlags::None)
89 , m_tracksParsingStatus(ParsingStatus::NotParsedYet)
90 , m_tagsParsingStatus(ParsingStatus::NotParsedYet)
91 , m_chaptersParsingStatus(ParsingStatus::NotParsedYet)
92 , m_attachmentsParsingStatus(ParsingStatus::NotParsedYet)
93 , m_minPadding(0)
94 , m_maxPadding(0)
95 , m_preferredPadding(0)
96 , m_tagPosition(ElementPosition::BeforeData)
97 , m_indexPosition(ElementPosition::BeforeData)
100 , m_maxFullParseSize(0x3200000)
101{
102}
103
108 : MediaFileInfo(std::string())
109{
110}
111
115MediaFileInfo::MediaFileInfo(std::string_view path)
116 : MediaFileInfo(std::string(path))
117{
118}
119
126
141{
142 CPP_UTILITIES_UNUSED(progress)
143
144 // skip if container format already parsed
146 return;
147 }
148
149 static const string context("parsing file header");
150 open(); // ensure the file is open
151 m_containerFormat = ContainerFormat::Unknown;
152
153 // file size
154 m_paddingSize = 0;
155 m_containerOffset = 0;
156 std::size_t bytesSkippedBeforeContainer = 0;
157 std::streamoff id3v2Size = 0;
158
159 // read signatrue
160 char buff[16];
161 const char *const buffEnd = buff + sizeof(buff), *buffOffset;
162startParsingSignature:
163 if (progress.isAborted()) {
164 diag.emplace_back(DiagLevel::Information, "Parsing the container format has been aborted.", context);
165 return;
166 }
167 if (size() - containerOffset() >= 16) {
168 stream().seekg(m_containerOffset, ios_base::beg);
169 stream().read(buff, sizeof(buff));
170
171 // skip zero/junk bytes
172 // notes:
173 // - Only skipping 4 or more consecutive zero bytes at this point because some signatures start with up to 4 zero bytes.
174 // - It seems that most players/tools¹ skip junk bytes, at least when reading MP3 files. Hence the tagparser library is following
175 // the same approach. (¹e.g. ffmpeg: "[mp3 @ 0x559e1f4cbd80] Skipping 1670 bytes of junk at 1165.")
176 std::size_t bytesSkipped = 0;
177 for (buffOffset = buff; buffOffset != buffEnd && !(*buffOffset); ++buffOffset, ++bytesSkipped)
178 ;
179 if (bytesSkipped >= 4) {
180 skipJunkBytes:
181 m_containerOffset += static_cast<std::streamoff>(bytesSkipped);
182 m_paddingSize += bytesSkipped;
183
184 // give up after 0x800 bytes
185 if ((bytesSkippedBeforeContainer += bytesSkipped) >= 0x800u) {
186 m_containerParsingStatus = ParsingStatus::NotSupported;
187 m_containerFormat = ContainerFormat::Unknown;
188 m_containerOffset = id3v2Size;
189 return;
190 }
191
192 // try again
193 goto startParsingSignature;
194 }
195
196 // parse signature
197 switch ((m_containerFormat = parseSignature(buff, sizeof(buff)))) {
199 // save position of ID3v2 tag
200 m_actualId3v2TagOffsets.push_back(m_containerOffset);
201 if (m_actualId3v2TagOffsets.size() == 2) {
202 diag.emplace_back(DiagLevel::Warning, "There is more than just one ID3v2 header at the beginning of the file.", context);
203 }
204
205 // read ID3v2 header
206 stream().seekg(m_containerOffset + 5, ios_base::beg);
207 stream().read(buff, 5);
208
209 // set the container offset to skip ID3v2 header
210 m_containerOffset += toNormalInt(BE::toInt<std::uint32_t>(buff + 1)) + 10;
211 if ((*buff) & 0x10) {
212 // footer present
213 m_containerOffset += 10;
214 }
215 id3v2Size = m_containerOffset;
216
217 // continue reading signature
218 goto startParsingSignature;
219
222 // MP4/QuickTime is handled using Mp4Container instance
223 m_container = make_unique<Mp4Container>(*this, m_containerOffset);
224 try {
225 static_cast<Mp4Container *>(m_container.get())->validateElementStructure(diag, progress, &m_paddingSize);
226 } catch (const OperationAbortedException &) {
227 diag.emplace_back(DiagLevel::Information, "Validating the MP4 element structure has been aborted.", context);
228 } catch (const Failure &) {
229 m_containerParsingStatus = ParsingStatus::CriticalFailure;
230 }
231 break;
232 }
234 // EBML/Matroska is handled using MatroskaContainer instance
235 auto container = make_unique<MatroskaContainer>(*this, m_containerOffset);
236 try {
237 container->parseHeader(diag, progress);
238 if (container->documentType() == "matroska") {
239 m_containerFormat = ContainerFormat::Matroska;
240 } else if (container->documentType() == "webm") {
241 m_containerFormat = ContainerFormat::Webm;
242 }
243 if (isForcingFullParse()) {
244 // validating the element structure of Matroska files takes too long when
245 // parsing big files so do this only when explicitly desired
246 container->validateElementStructure(diag, progress, &m_paddingSize);
247 container->validateIndex(diag, progress);
248 }
249 } catch (const OperationAbortedException &) {
250 diag.emplace_back(DiagLevel::Information, "Validating the Matroska element structure has been aborted.", context);
251 } catch (const Failure &) {
252 m_containerParsingStatus = ParsingStatus::CriticalFailure;
253 }
254 m_container = std::move(container);
255 break;
256 }
258 // Ogg is handled by OggContainer instance
259 m_container = make_unique<OggContainer>(*this, m_containerOffset);
260 static_cast<OggContainer *>(m_container.get())->setChecksumValidationEnabled(isForcingFullParse());
261 break;
264 // skip APE tag if the specified size makes sense at all
265 if (m_containerFormat == ContainerFormat::ApeTag) {
266 if (const auto apeEnd = m_containerOffset + 32 + LE::toUInt32(buff + 12); apeEnd <= static_cast<std::streamoff>(size())) {
267 // take record of APE tag
268 diag.emplace_back(DiagLevel::Critical,
269 argsToString("Found an APE tag at the beginning of the file at offset ", m_containerOffset,
270 ". This tag format is not supported and the tag will therefore be ignored. It will NOT be preserved when saving as "
271 "placing an APE tag at the beginning of a file is strongly unrecommended."),
272 context);
273 // continue reading signature
274 m_containerOffset = apeEnd;
275 goto startParsingSignature;
276 }
277 m_containerFormat = ContainerFormat::Unknown;
278 }
279
280 // check for magic numbers at odd offsets
281 // -> check for tar (magic number at offset 0x101)
282 if (size() > 0x107) {
283 stream().seekg(0x101);
284 stream().read(buff, 6);
285 if (buff[0] == 0x75 && buff[1] == 0x73 && buff[2] == 0x74 && buff[3] == 0x61 && buff[4] == 0x72 && buff[5] == 0x00) {
286 m_containerFormat = ContainerFormat::Tar;
287 break;
288 }
289 }
290 // skip previously determined zero-bytes or try our luck on the next byte
291 if (!bytesSkipped) {
292 ++bytesSkipped;
293 }
294 goto skipJunkBytes;
295 default:;
296 }
297 }
298
299 if (bytesSkippedBeforeContainer) {
300 diag.emplace_back(DiagLevel::Warning, argsToString(bytesSkippedBeforeContainer, " bytes of junk skipped"), context);
301 }
302
303 // set parsing status
304 if (m_containerParsingStatus == ParsingStatus::NotParsedYet) {
305 if (m_containerFormat == ContainerFormat::Unknown) {
306 m_containerParsingStatus = ParsingStatus::NotSupported;
307 } else {
308 m_containerParsingStatus = ParsingStatus::Ok;
309 }
310 }
311}
312
325{
326 // skip if tracks already parsed
328 return;
329 }
330 static const string context("parsing tracks");
331
332 try {
333 // parse tracks via container object
334 if (m_container) {
335 m_container->parseTracks(diag, progress);
336 m_tracksParsingStatus = ParsingStatus::Ok;
337 return;
338 }
339
340 // parse tracks via track object for "single-track"-formats
341 switch (m_containerFormat) {
343 m_singleTrack = make_unique<AdtsStream>(stream(), m_containerOffset);
344 break;
346 m_singleTrack = make_unique<FlacStream>(*this, m_containerOffset);
347 break;
349 m_singleTrack = make_unique<IvfStream>(stream(), m_containerOffset);
350 break;
352 m_singleTrack = make_unique<MpegAudioFrameStream>(stream(), m_containerOffset);
353 break;
355 m_singleTrack = make_unique<WaveAudioStream>(stream(), m_containerOffset);
356 break;
357 default:
359 }
360 if (m_containerFormat != ContainerFormat::Flac) {
361 // ensure the effective size has been determined
362 // note: This is not required for FLAC and should also be avoided as parseTags() will invoke
363 // parseTracks() when dealing with FLAC files.
364 parseTags(diag, progress);
365 m_singleTrack->setSize(m_effectiveSize);
366 }
367 m_singleTrack->parseHeader(diag, progress);
368
369 // take padding for some "single-track" formats into account
370 switch (m_containerFormat) {
372 m_paddingSize += static_cast<FlacStream *>(m_singleTrack.get())->paddingSize();
373 break;
374 default:;
375 }
376
377 m_tracksParsingStatus = ParsingStatus::Ok;
378
379 } catch (const NotImplementedException &) {
380 diag.emplace_back(DiagLevel::Information, "Parsing tracks is not implemented for the container format of the file.", context);
381 m_tracksParsingStatus = ParsingStatus::NotSupported;
382 } catch (const OperationAbortedException &) {
383 diag.emplace_back(DiagLevel::Information, "Parsing tracks has been aborted.", context);
384 } catch (const Failure &) {
385 diag.emplace_back(DiagLevel::Critical, "Unable to parse tracks.", context);
386 m_tracksParsingStatus = ParsingStatus::CriticalFailure;
387 }
388}
389
403{
404 // skip if tags already parsed
406 return;
407 }
408 static const string context("parsing tag");
409
410 // check for ID3v1 tag
411 auto effectiveSize = static_cast<std::streamoff>(size());
412 if (effectiveSize >= 128) {
413 m_id3v1Tag = make_unique<Id3v1Tag>();
414 try {
415 stream().seekg(effectiveSize - 128, std::ios_base::beg);
416 m_id3v1Tag->parse(stream(), diag);
418 effectiveSize -= 128;
419 } catch (const NoDataFoundException &) {
420 m_id3v1Tag.reset();
421 } catch (const OperationAbortedException &) {
422 diag.emplace_back(DiagLevel::Information, "Parsing ID3v1 tag has been aborted.", context);
423 return;
424 } catch (const Failure &) {
425 m_tagsParsingStatus = ParsingStatus::CriticalFailure;
426 diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v1 tag.", context);
427 }
428 }
429
430 // check for APE tag at the end of the file (APE tags a the beginning are already covered when parsing the container format)
431 if (constexpr auto apeHeaderSize = 32; effectiveSize >= apeHeaderSize) {
432 const auto footerOffset = effectiveSize - apeHeaderSize;
433 char buffer[apeHeaderSize];
434 stream().seekg(footerOffset, std::ios_base::beg);
435 stream().read(buffer, sizeof(buffer));
436 if (BE::toInt<std::uint64_t>(buffer) == 0x4150455441474558ul /* APETAGEX */) {
437 // take record of APE tag
438 const auto tagSize = static_cast<std::streamoff>(LE::toInt<std::uint32_t>(buffer + 12));
439 const auto flags = LE::toInt<std::uint32_t>(buffer + 20);
440 // subtract tag size (footer size and contents) from effective size
441 if (tagSize <= effectiveSize) {
442 effectiveSize -= tagSize;
443 }
444 // subtract header size (not included in tag size) from effective size if flags indicate presence of header
445 if ((flags & 0x80000000u) && (apeHeaderSize <= effectiveSize)) {
446 effectiveSize -= apeHeaderSize;
447 }
448 diag.emplace_back(DiagLevel::Warning,
449 argsToString("Found an APE tag at the end of the file at offset ", (footerOffset - tagSize),
450 ". This tag format is not supported and the tag will therefore be ignored. It will be preserved when saving as-is."),
451 context);
452 }
453 }
454
455 // check for ID3v2 tags: the offsets of the ID3v2 tags have already been parsed when parsing the container format
456 m_id3v2Tags.clear();
457 for (const auto offset : m_actualId3v2TagOffsets) {
458 auto id3v2Tag = make_unique<Id3v2Tag>();
459 stream().seekg(offset, ios_base::beg);
460 try {
461 id3v2Tag->parse(stream(), size() - static_cast<std::uint64_t>(offset), diag);
462 m_paddingSize += id3v2Tag->paddingSize();
463 } catch (const NoDataFoundException &) {
464 continue;
465 } catch (const OperationAbortedException &) {
466 diag.emplace_back(DiagLevel::Information, "Parsing ID3v2 tags has been aborted.", context);
467 return;
468 } catch (const Failure &) {
469 m_tagsParsingStatus = ParsingStatus::CriticalFailure;
470 diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v2 tag.", context);
471 }
472 m_id3v2Tags.emplace_back(id3v2Tag.release());
473 }
474
475 // compute effective size
476 m_effectiveSize = static_cast<std::uint64_t>(effectiveSize - m_containerOffset);
477
478 // check for tags in tracks (FLAC only) or via container object
479 try {
480 if (m_containerFormat == ContainerFormat::Flac) {
481 parseTracks(diag, progress);
482 if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
483 m_tagsParsingStatus = m_tracksParsingStatus;
484 }
485 return;
486 } else if (m_container) {
487 m_container->parseTags(diag, progress);
488 } else if (m_containerFormat != ContainerFormat::MpegAudioFrames) {
490 }
491
492 // set status, but do not override error/unsupported status form ID3 tags here
493 if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
494 m_tagsParsingStatus = ParsingStatus::Ok;
495 }
496
497 } catch (const NotImplementedException &) {
498 // set status to not supported, but do not override parsing status from ID3 tags here
499 if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) {
500 m_tagsParsingStatus = ParsingStatus::NotSupported;
501 }
502 diag.emplace_back(DiagLevel::Information, "Parsing tags is not implemented for the container format of the file.", context);
503 } catch (const OperationAbortedException &) {
504 diag.emplace_back(DiagLevel::Information, "Parsing tags from container/streams has been aborted.", context);
505 return;
506 } catch (const Failure &) {
507 m_tagsParsingStatus = ParsingStatus::CriticalFailure;
508 diag.emplace_back(DiagLevel::Critical, "Unable to parse tag.", context);
509 }
510}
511
522{
523 // skip if chapters already parsed
525 return;
526 }
527 static const string context("parsing chapters");
528
529 try {
530 // parse chapters via container object
531 if (!m_container) {
533 }
534 m_container->parseChapters(diag, progress);
535 m_chaptersParsingStatus = ParsingStatus::Ok;
536 } catch (const NotImplementedException &) {
537 m_chaptersParsingStatus = ParsingStatus::NotSupported;
538 diag.emplace_back(DiagLevel::Information, "Parsing chapters is not implemented for the container format of the file.", context);
539 } catch (const Failure &) {
540 m_chaptersParsingStatus = ParsingStatus::CriticalFailure;
541 diag.emplace_back(DiagLevel::Critical, "Unable to parse chapters.", context);
542 }
543}
544
555{
556 // skip if attachments already parsed
558 return;
559 }
560 static const string context("parsing attachments");
561
562 try {
563 // parse attachments via container object
564 if (!m_container) {
566 }
567 m_container->parseAttachments(diag, progress);
568 m_attachmentsParsingStatus = ParsingStatus::Ok;
569 } catch (const NotImplementedException &) {
570 m_attachmentsParsingStatus = ParsingStatus::NotSupported;
571 diag.emplace_back(DiagLevel::Information, "Parsing attachments is not implemented for the container format of the file.", context);
572 } catch (const Failure &) {
573 m_attachmentsParsingStatus = ParsingStatus::CriticalFailure;
574 diag.emplace_back(DiagLevel::Critical, "Unable to parse attachments.", context);
575 }
576}
577
585{
586 parseContainerFormat(diag, progress);
587 if (progress.isAborted()) {
588 return;
589 }
590 parseTracks(diag, progress);
591 if (progress.isAborted()) {
592 return;
593 }
594 parseTags(diag, progress);
595 if (progress.isAborted()) {
596 return;
597 }
598 parseChapters(diag, progress);
599 if (progress.isAborted()) {
600 return;
601 }
602 parseAttachments(diag, progress);
603}
604
617{
618 // check if tags have been parsed yet (tags must have been parsed yet to create appropriate tags)
620 return false;
621 }
622
623 // check if tags need to be created/adjusted/removed
624 const auto requiredTargets(settings.requiredTargets);
625 const auto flags(settings.flags);
626 const auto targetsRequired = !requiredTargets.empty() && (requiredTargets.size() != 1 || !requiredTargets.front().isEmpty());
627 auto targetsSupported = false;
628 if (areTagsSupported() && m_container) {
629 // container object takes care of tag management
630 if (targetsRequired) {
631 // check whether container supports targets
632 if (m_container->tagCount()) {
633 // all tags in the container should support targets if the first one supports targets
634 targetsSupported = m_container->tag(0)->supportsTarget();
635 } else {
636 // try to create a new tag and check whether targets are supported
637 auto *const tag = m_container->createTag();
638 if (tag && (targetsSupported = tag->supportsTarget())) {
639 tag->setTarget(requiredTargets.front());
640 }
641 }
642 if (targetsSupported) {
643 for (const auto &target : requiredTargets) {
644 m_container->createTag(target);
645 }
646 }
647 } else {
648 // no targets are required -> just ensure that at least one tag is present
649 m_container->createTag();
650 }
651 return true;
652 }
653
654 // no container object present
655 switch (m_containerFormat) {
657 static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
658 break;
659 default:
660 // create ID3 tag(s)
662 switch (containerFormat()) {
667 break;
668 default:
669 return false;
670 }
671 }
672 // create ID3 tags according to id3v2usage and id3v2usage
673 // always create ID3v1 tag -> ensure there is one
674 if (settings.id3v1usage == TagUsage::Always && !id3v1Tag()) {
675 auto *const id3v1Tag = createId3v1Tag();
677 for (const auto &id3v2Tag : id3v2Tags()) {
678 // overwrite existing values to ensure default ID3v1 genre "Blues" is updated as well
679 id3v1Tag->insertValues(*id3v2Tag, true);
680 // ID3v1 does not support all text encodings which might be used in ID3v2
682 }
683 }
684 }
685 if (settings.id3v2usage == TagUsage::Always && !hasId3v2Tag()) {
686 // always create ID3v2 tag -> ensure there is one and set version
687 auto *const id3v2Tag = createId3v2Tag();
688 id3v2Tag->setVersion(settings.id3v2MajorVersion, 0);
689 if ((flags & TagCreationFlags::Id3InitOnCreate) && id3v1Tag()) {
690 id3v2Tag->insertValues(*id3v1Tag(), true);
691 }
692 }
693 }
694
697 }
698 // remove ID3 tags according to settings
699 if (settings.id3v1usage == TagUsage::Never && hasId3v1Tag()) {
700 // transfer tags to ID3v2 tag before removing
702 id3v2Tags().front()->insertValues(*id3v1Tag(), false);
703 }
705 }
706 if (settings.id3v2usage == TagUsage::Never) {
708 // transfer tags to ID3v1 tag before removing
709 for (const auto &tag : id3v2Tags()) {
710 id3v1Tag()->insertValues(*tag, false);
711 }
712 }
714 } else if (!(flags & TagCreationFlags::KeepExistingId3v2Version)) {
715 // set version of ID3v2 tag according user preferences
716 for (const auto &tag : id3v2Tags()) {
717 tag->setVersion(settings.id3v2MajorVersion, 0);
718 }
719 }
720 return true;
721}
722
744{
745 static const string context("making file");
746 diag.emplace_back(DiagLevel::Information, "Changes are about to be applied.", context);
747 bool previousParsingSuccessful = true;
748 switch (tagsParsingStatus()) {
751 break;
752 default:
753 previousParsingSuccessful = false;
754 diag.emplace_back(DiagLevel::Critical, "Tags have to be parsed without critical errors before changes can be applied.", context);
755 }
756 switch (tracksParsingStatus()) {
759 break;
760 default:
761 previousParsingSuccessful = false;
762 diag.emplace_back(DiagLevel::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context);
763 }
764 if (!previousParsingSuccessful) {
765 throw InvalidDataException();
766 }
767 if (m_container) { // container object takes care
768 // ID3 tags can not be applied in this case -> add warnings if ID3 tags have been assigned
769 if (hasId3v1Tag()) {
770 diag.emplace_back(DiagLevel::Warning, "Assigned ID3v1 tag can't be attached and will be ignored.", context);
771 }
772 if (hasId3v2Tag()) {
773 diag.emplace_back(DiagLevel::Warning, "Assigned ID3v2 tag can't be attached and will be ignored.", context);
774 }
775 m_tracksParsingStatus = ParsingStatus::NotParsedYet;
776 m_tagsParsingStatus = ParsingStatus::NotParsedYet;
777 try {
778 m_container->makeFile(diag, progress);
779 } catch (...) {
780 // since the file might be messed up, invalidate the parsing results
782 throw;
783 }
784 } else { // implementation if no container object is present
785 // assume the file is a MP3 file
786 try {
787 makeMp3File(diag, progress);
788 } catch (...) {
789 // since the file might be messed up, invalidate the parsing results
791 throw;
792 }
793 }
795}
796
810{
811 MediaType mediaType = MediaType::Unknown;
812 unsigned int version = 0;
813 switch (m_containerFormat) {
815 // check for video track or whether only Opus or Speex tracks are present
816 const auto &tracks = static_cast<OggContainer *>(m_container.get())->tracks();
817 if (tracks.empty()) {
818 break;
819 }
820 bool onlyOpus = true, onlySpeex = true;
821 for (const auto &track : static_cast<OggContainer *>(m_container.get())->tracks()) {
822 if (track->mediaType() == MediaType::Video) {
823 mediaType = MediaType::Video;
824 }
825 if (track->format().general != GeneralMediaFormat::Opus) {
826 onlyOpus = false;
827 }
828 if (track->format().general != GeneralMediaFormat::Speex) {
829 onlySpeex = false;
830 }
831 }
832 if (onlyOpus) {
833 version = static_cast<unsigned int>(GeneralMediaFormat::Opus);
834 } else if (onlySpeex) {
835 version = static_cast<unsigned int>(GeneralMediaFormat::Speex);
836 }
837 break;
838 }
842 break;
844 if (m_singleTrack) {
845 version = m_singleTrack->format().sub;
846 }
847 break;
848 default:;
849 }
850 return TagParser::containerFormatAbbreviation(m_containerFormat, mediaType, version);
851}
852
863string_view MediaFileInfo::mimeType() const
864{
865 MediaType mediaType;
866 switch (m_containerFormat) {
871 break;
872 default:
873 mediaType = MediaType::Unknown;
874 }
875 return TagParser::containerMimeType(m_containerFormat, mediaType);
876}
877
890vector<AbstractTrack *> MediaFileInfo::tracks() const
891{
892 vector<AbstractTrack *> res;
893 size_t trackCount = 0;
894 size_t containerTrackCount = 0;
895 if (m_singleTrack) {
896 trackCount = 1;
897 }
898 if (m_container) {
899 trackCount += (containerTrackCount = m_container->trackCount());
900 }
901 res.reserve(trackCount);
902
903 if (m_singleTrack) {
904 res.push_back(m_singleTrack.get());
905 }
906 for (size_t i = 0; i != containerTrackCount; ++i) {
907 res.push_back(m_container->track(i));
908 }
909 return res;
910}
911
921{
923 return false;
924 }
925 if (m_singleTrack && m_singleTrack->mediaType() == type) {
926 return true;
927 } else if (m_container) {
928 for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) {
929 if (m_container->track(i)->mediaType() == type) {
930 return true;
931 }
932 }
933 }
934 return false;
935}
936
946CppUtilities::TimeSpan MediaFileInfo::duration() const
947{
948 if (m_container) {
949 return m_container->duration();
950 } else if (m_singleTrack) {
951 return m_singleTrack->duration();
952 }
953 return TimeSpan();
954}
955
966{
967 const auto duration = this->duration();
968 if (duration.isNull()) {
969 return 0.0;
970 }
971 return 0.0078125 * static_cast<double>(size()) / duration.totalSeconds();
972}
973
984unordered_set<string> MediaFileInfo::availableLanguages(MediaType type) const
985{
986 unordered_set<string> res;
987 if (m_container) {
988 for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) {
989 const AbstractTrack *const track = m_container->track(i);
990 if (type != MediaType::Unknown && track->mediaType() != type) {
991 continue;
992 }
993 if (const auto &language = track->locale().someAbbreviatedName(); !language.empty()) {
994 res.emplace(language);
995 }
996 }
997 } else if (m_singleTrack && (type == MediaType::Unknown || m_singleTrack->mediaType() == type)) {
998 if (const auto &language = m_singleTrack->locale().someAbbreviatedName(); !language.empty()) {
999 res.emplace(language);
1000 }
1001 }
1002 return res;
1003}
1004
1017{
1018 if (m_container) {
1019 const size_t trackCount = m_container->trackCount();
1020 vector<string> parts;
1021 parts.reserve(trackCount);
1022 for (size_t i = 0; i != trackCount; ++i) {
1023 const string description(m_container->track(i)->description());
1024 if (!description.empty()) {
1025 parts.emplace_back(std::move(description));
1026 }
1027 }
1028 return joinStrings(parts, " / ");
1029 } else if (m_singleTrack) {
1030 return m_singleTrack->description();
1031 }
1032 return string();
1033}
1034
1045{
1047 return false;
1048 }
1049 if (m_id3v1Tag) {
1050 m_id3v1Tag.reset();
1051 return true;
1052 }
1053 return false;
1054}
1055
1072{
1074 return nullptr;
1075 }
1076 if (!m_id3v1Tag) {
1077 m_id3v1Tag = make_unique<Id3v1Tag>();
1078 }
1079 return m_id3v1Tag.get();
1080}
1081
1093{
1095 return false;
1096 }
1097 for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) {
1098 if (i->get() == tag) {
1099 m_id3v2Tags.erase(i);
1100 return true;
1101 }
1102 }
1103 return false;
1104}
1105
1115{
1116 if (tagsParsingStatus() == ParsingStatus::NotParsedYet || m_id3v2Tags.empty()) {
1117 return false;
1118 }
1119 m_id3v2Tags.clear();
1120 return true;
1121}
1122
1139{
1140 if (m_id3v2Tags.empty()) {
1141 m_id3v2Tags.emplace_back(make_unique<Id3v2Tag>());
1142 }
1143 return m_id3v2Tags.front().get();
1144}
1145
1160{
1161 if (!tag) {
1162 return false;
1163 }
1164
1165 // remove tag via container
1166 if (m_container) {
1167 return m_container->removeTag(tag);
1168 }
1169
1170 // remove tag via track for "single-track" formats
1171 if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) {
1172 auto *const flacStream(static_cast<FlacStream *>(m_singleTrack.get()));
1173 if (flacStream->vorbisComment() == tag) {
1174 return flacStream->removeVorbisComment();
1175 }
1176 }
1177
1178 // remove ID3 tags
1179 if (m_id3v1Tag.get() == tag) {
1180 m_id3v1Tag.reset();
1181 return true;
1182 }
1183 for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) {
1184 if (i->get() == tag) {
1185 m_id3v2Tags.erase(i);
1186 return true;
1187 }
1188 }
1189 return false;
1190}
1191
1199{
1200 if (m_container) {
1201 m_container->removeAllTags();
1202 }
1203 if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) {
1204 static_cast<FlacStream *>(m_singleTrack.get())->removeVorbisComment();
1205 }
1206 m_id3v1Tag.reset();
1207 m_id3v2Tags.clear();
1208}
1209
1214{
1215 if (m_container && m_container->chapterCount()) {
1216 return true;
1217 }
1218 switch (m_containerFormat) {
1221 return true;
1222 default:
1223 return false;
1224 }
1225}
1226
1231{
1232 if (m_container && m_container->attachmentCount()) {
1233 return true;
1234 }
1235 switch (m_containerFormat) {
1238 return true;
1239 default:
1240 return false;
1241 }
1242}
1243
1248{
1249 if (trackCount()) {
1250 return true;
1251 }
1252 switch (m_containerFormat) {
1259 return true;
1260 default:
1261 return false;
1262 }
1263}
1264
1269{
1270 switch (m_containerFormat) {
1279 // these container formats are supported
1280 return true;
1281 default:
1282 // the container format is unsupported
1283 // -> an ID3 tag might be already present, in this case the tags are considered supported
1284 return !m_container && (hasId3v1Tag() || hasId3v2Tag());
1285 }
1286}
1287
1294{
1295 // simply return the first tag here since MP4 files never contain multiple tags
1296 return (m_containerFormat == ContainerFormat::Mp4 || m_containerFormat == ContainerFormat::QuickTime) && m_container
1297 && m_container->tagCount() > 0
1298 ? static_cast<Mp4Container *>(m_container.get())->tags().front().get()
1299 : nullptr;
1300}
1301
1308const vector<unique_ptr<MatroskaTag>> &MediaFileInfo::matroskaTags() const
1309{
1310 // matroska files might contain multiple tags (targeting different scopes)
1311 if (m_containerFormat == ContainerFormat::Matroska && m_container) {
1312 return static_cast<MatroskaContainer *>(m_container.get())->tags();
1313 } else {
1314 static const std::vector<std::unique_ptr<MatroskaTag>> empty;
1315 return empty;
1316 }
1317}
1318
1325{
1326 return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount()
1327 ? static_cast<OggContainer *>(m_container.get())->tags().front().get()
1328 : (m_containerFormat == ContainerFormat::Flac && m_singleTrack ? static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment() : nullptr);
1329}
1330
1336vector<AbstractChapter *> MediaFileInfo::chapters() const
1337{
1338 vector<AbstractChapter *> res;
1339 if (m_container) {
1340 const size_t count = m_container->chapterCount();
1341 res.reserve(count);
1342 for (size_t i = 0; i != count; ++i) {
1343 res.push_back(m_container->chapter(i));
1344 }
1345 }
1346 return res;
1347}
1348
1354vector<AbstractAttachment *> MediaFileInfo::attachments() const
1355{
1356 vector<AbstractAttachment *> res;
1357 if (m_container) {
1358 const size_t count = m_container->attachmentCount();
1359 res.reserve(count);
1360 for (size_t i = 0; i != count; ++i) {
1361 res.push_back(m_container->attachment(i));
1362 }
1363 }
1364 return res;
1365}
1366
1379{
1380 m_containerParsingStatus = ParsingStatus::NotParsedYet;
1381 m_containerFormat = ContainerFormat::Unknown;
1382 m_containerOffset = 0;
1383 m_paddingSize = 0;
1384 m_tracksParsingStatus = ParsingStatus::NotParsedYet;
1385 m_tagsParsingStatus = ParsingStatus::NotParsedYet;
1386 m_chaptersParsingStatus = ParsingStatus::NotParsedYet;
1387 m_attachmentsParsingStatus = ParsingStatus::NotParsedYet;
1388 m_id3v1Tag.reset();
1389 m_id3v2Tags.clear();
1390 m_actualId3v2TagOffsets.clear();
1391 m_fileStructureFlags = MediaFileStructureFlags::None;
1392 m_container.reset();
1393 m_singleTrack.reset();
1394}
1395
1399void MediaFileInfo::writePadding(std::ostream &outputStream, std::uint64_t size)
1400{
1401 static constexpr auto bufferSize = static_cast<std::uint64_t>(1024 * 8);
1402 for (constexpr char buffer[bufferSize] = {}; size;) {
1403 const auto count = std::min(bufferSize, size);
1404 outputStream.write(buffer, static_cast<std::streamsize>(count));
1405 size -= count;
1406 }
1407}
1408
1426{
1427 auto begin = m_id3v2Tags.begin(), end = m_id3v2Tags.end();
1428 if (begin == end) {
1429 return;
1430 }
1431 Id3v2Tag &first = **begin;
1432 auto isecond = begin + 1;
1433 if (isecond == end) {
1434 return;
1435 }
1436 for (auto i = isecond; i != end; ++i) {
1437 first.insertFields(**i, false);
1438 }
1439 m_id3v2Tags.erase(isecond, end);
1440}
1441
1460
1479
1495{
1496 switch (m_containerFormat) {
1498 if (m_container) {
1499 return static_cast<OggContainer *>(m_container.get())->createTag(TagTarget());
1500 }
1501 break;
1503 if (m_singleTrack) {
1504 return static_cast<FlacStream *>(m_singleTrack.get())->createVorbisComment();
1505 }
1506 break;
1507 default:;
1508 }
1509 return nullptr;
1510}
1511
1522{
1523 switch (m_containerFormat) {
1525 if (m_container) {
1526 bool hadTags = static_cast<OggContainer *>(m_container.get())->tagCount();
1527 static_cast<OggContainer *>(m_container.get())->removeAllTags();
1528 return hadTags;
1529 }
1530 break;
1532 if (m_singleTrack) {
1533 return static_cast<FlacStream *>(m_singleTrack.get())->removeVorbisComment();
1534 }
1535 break;
1536 default:;
1537 }
1538 return false;
1539}
1540
1550void MediaFileInfo::tags(std::vector<Tag *> &tags) const
1551{
1552 if (hasId3v1Tag()) {
1553 tags.push_back(m_id3v1Tag.get());
1554 }
1555 for (const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
1556 tags.push_back(tag.get());
1557 }
1558 if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) {
1559 if (auto *const vorbisComment = static_cast<const FlacStream *>(m_singleTrack.get())->vorbisComment()) {
1560 tags.push_back(vorbisComment);
1561 }
1562 }
1563 if (m_container) {
1564 for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
1565 tags.push_back(m_container->tag(i));
1566 }
1567 }
1568}
1569
1578vector<Tag *> MediaFileInfo::tags() const
1579{
1580 auto res = vector<Tag *>();
1581 tags(res);
1582 return res;
1583}
1584
1592{
1593 return hasId3v1Tag() || hasId3v2Tag() || (m_container && m_container->tagCount())
1594 || (m_containerFormat == ContainerFormat::Flac && static_cast<FlacStream *>(m_singleTrack.get())->vorbisComment());
1595}
1596
1606void MediaFileInfo::parsedTags(std::vector<Tag *> &tags) const
1607{
1608 if (hasId3v1Tag() && m_id3v1Tag->size()) {
1609 tags.push_back(m_id3v1Tag.get());
1610 }
1611 for (const unique_ptr<Id3v2Tag> &tag : m_id3v2Tags) {
1612 if (tag->size()) {
1613 tags.push_back(tag.get());
1614 }
1615 }
1616 if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) {
1617 if (auto *const vorbisComment = static_cast<const FlacStream *>(m_singleTrack.get())->vorbisComment()) {
1618 if (vorbisComment->size()) {
1619 tags.push_back(vorbisComment);
1620 }
1621 }
1622 }
1623 if (m_container) {
1624 for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) {
1625 if (auto *const tag = m_container->tag(i); tag->size()) {
1626 tags.push_back(tag);
1627 }
1628 }
1629 }
1630}
1631
1640std::vector<Tag *> MediaFileInfo::parsedTags() const
1641{
1642 auto res = vector<Tag *>();
1643 parsedTags(res);
1644 return res;
1645}
1646
1655
1659void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &progress)
1660{
1661 static const string context("making MP3/FLAC file");
1662
1663 // don't rewrite the complete file if there are no ID3v2/FLAC tags present or to be written
1664 if (!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty()
1665 && m_containerFormat != ContainerFormat::Flac) {
1666 // alter ID3v1 tag
1667 if (!m_id3v1Tag) {
1668 // remove ID3v1 tag
1669 if (!(m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag)) {
1670 diag.emplace_back(DiagLevel::Information, "Nothing to be changed.", context);
1671 return;
1672 }
1673 progress.updateStep("Removing ID3v1 tag ...");
1674 stream().close();
1675 auto ec = std::error_code();
1676 std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), size() - 128, ec);
1677 if (!ec) {
1678 reportSizeChanged(size() - 128);
1679 } else {
1680 diag.emplace_back(DiagLevel::Critical, "Unable to truncate file to remove ID3v1 tag: " + ec.message(), context);
1681 throw std::ios_base::failure("Unable to truncate file to remove ID3v1 tag.");
1682 }
1683 return;
1684 } else {
1685 // add or update ID3v1 tag
1686 if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) {
1687 progress.updateStep("Updating existing ID3v1 tag ...");
1688 // ensure the file is still open / not readonly
1689 open();
1690 stream().seekp(-128, ios_base::end);
1691 try {
1692 m_id3v1Tag->make(stream(), diag);
1693 } catch (const Failure &) {
1694 diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
1695 }
1696 } else {
1697 progress.updateStep("Adding new ID3v1 tag ...");
1698 // ensure the file is still open / not readonly
1699 open();
1700 stream().seekp(0, ios_base::end);
1701 try {
1702 m_id3v1Tag->make(stream(), diag);
1703 } catch (const Failure &) {
1704 diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
1705 }
1706 }
1707 // prevent deferring final write operations (to catch and handle possible errors here)
1708 stream().flush();
1709 }
1710 return;
1711 }
1712
1713 // ID3v2 needs to be modified
1714 FlacStream *const flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast<FlacStream *>(m_singleTrack.get()) : nullptr);
1715 progress.updateStep(flacStream ? "Updating FLAC tags ..." : "Updating ID3v2 tags ...");
1716
1717 // prepare ID3v2 tags
1718 auto makers = std::vector<Id3v2TagMaker>();
1719 makers.reserve(m_id3v2Tags.size());
1720 auto tagsSize = std::uint64_t();
1721 for (auto &tag : m_id3v2Tags) {
1722 try {
1723 makers.emplace_back(tag->prepareMaking(diag));
1724 tagsSize += makers.back().requiredSize();
1725 } catch (const Failure &) {
1726 }
1727 }
1728
1729 // determine stream offset and make track/format specific metadata
1730 auto streamOffset = std::uint32_t(); // where the actual stream starts
1731 auto flacMetaData = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary);
1732 flacMetaData.exceptions(std::ios_base::badbit | std::ios_base::failbit);
1733 auto startOfLastMetaDataBlock = std::streamoff();
1734 if (flacStream) {
1735 // if it is a raw FLAC stream, make FLAC metadata
1736 startOfLastMetaDataBlock = flacStream->makeHeader(flacMetaData, diag);
1737 tagsSize += static_cast<std::uint64_t>(flacMetaData.tellp());
1738 streamOffset = flacStream->streamOffset();
1739 } else {
1740 // make no further metadata, just use the container offset as stream offset
1741 streamOffset = static_cast<std::uint32_t>(m_containerOffset);
1742 }
1743
1744 // check whether rewrite is required
1745 auto rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > streamOffset);
1746 auto padding = std::size_t();
1747 if (!rewriteRequired) {
1748 // rewriting is not forced and new tag is not too big for available space
1749 // -> calculate new padding
1750 padding = streamOffset - tagsSize;
1751 // -> check whether the new padding matches specifications
1752 if (padding < minPadding() || padding > maxPadding()) {
1753 rewriteRequired = true;
1754 }
1755 }
1756 if (makers.empty() && !flacStream) {
1757 // an ID3v2 tag is not written and it is not a FLAC stream
1758 // -> can't include padding
1759 if (padding) {
1760 // but padding would be present -> need to rewrite
1761 padding = 0; // can't write the preferred padding despite rewriting
1762 rewriteRequired = true;
1763 }
1764 } else if (rewriteRequired) {
1765 // rewriting is forced or new ID3v2 tag is too big for available space
1766 // -> use preferred padding when rewriting anyways
1767 padding = preferredPadding();
1768 } else if (makers.empty() && flacStream && padding && padding < 4) {
1769 // no ID3v2 tag -> must include padding in FLAC stream
1770 // but padding of 1, 2, and 3 byte isn't possible -> need to rewrite
1771 padding = preferredPadding();
1772 rewriteRequired = true;
1773 }
1774 if (rewriteRequired && flacStream && makers.empty() && padding) {
1775 // the first 4 byte of FLAC padding actually don't count because these
1776 // can not be used for additional meta data
1777 padding += 4;
1778 }
1779 progress.updateStep(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ...");
1780
1781 // setup stream(s) for writing
1782 // -> define variables needed to handle output stream and backup stream (required when rewriting the file)
1783 string originalPath = path(), backupPath;
1784 NativeFileStream &outputStream = stream();
1785 NativeFileStream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
1786
1787 if (rewriteRequired) {
1788 if (m_saveFilePath.empty()) {
1789 // move current file to temp dir and reopen it as backupStream, recreate original file
1790 try {
1791 BackupHelper::createBackupFileCanonical(backupDirectory(), originalPath, backupPath, outputStream, backupStream);
1792 // recreate original file, define buffer variables
1793 outputStream.open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc);
1794 } catch (const std::ios_base::failure &failure) {
1795 diag.emplace_back(
1796 DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context);
1797 throw;
1798 }
1799 } else {
1800 // open the current file as backupStream and create a new outputStream at the specified "save file path"
1801 try {
1802 close();
1803 backupStream.exceptions(ios_base::badbit | ios_base::failbit);
1804 backupStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::binary);
1805 outputStream.open(BasicFileInfo::pathForOpen(m_saveFilePath).data(), ios_base::out | ios_base::binary | ios_base::trunc);
1806 } catch (const std::ios_base::failure &failure) {
1807 diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context);
1808 throw;
1809 }
1810 }
1811
1812 } else { // !rewriteRequired
1813 // reopen original file to ensure it is opened for writing
1814 try {
1815 close();
1816 outputStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::out | ios_base::binary);
1817 } catch (const std::ios_base::failure &failure) {
1818 diag.emplace_back(DiagLevel::Critical, argsToString("Opening the file with write permissions failed: ", failure.what()), context);
1819 throw;
1820 }
1821 }
1822 // TODO: fix code duplication
1823
1824 // start actual writing
1825 try {
1826 // ensure we can cast padding safely to uint32
1827 if (padding > numeric_limits<std::uint32_t>::max()) {
1828 padding = numeric_limits<std::uint32_t>::max();
1829 diag.emplace_back(
1830 DiagLevel::Critical, argsToString("Preferred padding is not supported. Setting preferred padding to ", padding, '.'), context);
1831 }
1832
1833 if (!makers.empty()) {
1834 // write ID3v2 tags
1835 progress.updateStep("Writing ID3v2 tag ...");
1836 for (auto i = makers.begin(), end = makers.end() - 1; i != end; ++i) {
1837 i->make(outputStream, 0, diag);
1838 }
1839 // include padding into the last ID3v2 tag
1840 makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : static_cast<std::uint32_t>(padding), diag);
1841 }
1842
1843 if (flacStream) {
1844 if (padding && startOfLastMetaDataBlock) {
1845 // if appending padding, ensure the last flag of the last "METADATA_BLOCK_HEADER" is not set
1846 flacMetaData.seekg(startOfLastMetaDataBlock);
1847 flacMetaData.seekp(startOfLastMetaDataBlock);
1848 flacMetaData.put(static_cast<std::uint8_t>(flacMetaData.peek()) & (0x80u - 1));
1849 flacMetaData.seekg(0);
1850 }
1851
1852 // write FLAC metadata
1853 outputStream << flacMetaData.rdbuf();
1854
1855 // write padding
1856 if (padding) {
1857 flacStream->makePadding(outputStream, static_cast<std::uint32_t>(padding), true, diag);
1858 }
1859 }
1860
1861 if (makers.empty() && !flacStream) {
1862 writePadding(outputStream, padding);
1863 }
1864
1865 // copy / skip actual stream data
1866 // -> determine media data size
1867 std::uint64_t mediaDataSize = size() - streamOffset;
1868 if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) {
1869 mediaDataSize -= 128;
1870 }
1871
1872 if (rewriteRequired) {
1873 // copy data from original file
1874 switch (m_containerFormat) {
1876 progress.updateStep("Writing MPEG audio frames ...");
1877 break;
1878 default:
1879 progress.updateStep("Writing data ...");
1880 }
1881 backupStream.seekg(static_cast<streamoff>(streamOffset));
1882 CopyHelper<0x4000> copyHelper;
1883 copyHelper.callbackCopy(backupStream, stream(), mediaDataSize, std::bind(&AbortableProgressFeedback::isAborted, std::ref(progress)),
1884 std::bind(&AbortableProgressFeedback::updateStepPercentage, std::ref(progress), _1));
1885 } else {
1886 // just skip actual stream data
1887 outputStream.seekp(static_cast<std::streamoff>(mediaDataSize), ios_base::cur);
1888 }
1889
1890 // write ID3v1 tag
1891 if (m_id3v1Tag) {
1892 progress.updateStep("Writing ID3v1 tag ...");
1893 try {
1894 m_id3v1Tag->make(stream(), diag);
1895 } catch (const Failure &) {
1896 diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context);
1897 }
1898 }
1899
1900 // handle streams
1901 if (rewriteRequired) {
1902 // report new size
1903 reportSizeChanged(static_cast<std::uint64_t>(outputStream.tellp()));
1904 // "save as path" is now the regular path
1905 if (!saveFilePath().empty()) {
1907 m_saveFilePath.clear();
1908 }
1909 // prevent deferring final write operations (to catch and handle possible errors here); stream is useless for further
1910 // usage anyways because it is write-only
1911 outputStream.close();
1912 } else {
1913 const auto newSize = static_cast<std::uint64_t>(outputStream.tellp());
1914 if (newSize < size()) {
1915 // file is smaller after the modification -> truncate
1916 // -> prevent deferring final write operations
1917 outputStream.close();
1918 // -> truncate file
1919 auto ec = std::error_code();
1920 std::filesystem::resize_file(makeNativePath(BasicFileInfo::pathForOpen(path())), newSize, ec);
1921 if (!ec) {
1922 reportSizeChanged(newSize);
1923 } else {
1924 diag.emplace_back(DiagLevel::Critical, "Unable to truncate the file: " + ec.message(), context);
1925 }
1926 } else {
1927 // file is longer after the modification
1928 // -> prevent deferring final write operations (to catch and handle possible errors here)
1929 outputStream.flush();
1930 // -> report new size
1931 reportSizeChanged(newSize);
1932 }
1933 }
1934
1935 } catch (...) {
1936 BackupHelper::handleFailureAfterFileModifiedCanonical(*this, originalPath, backupPath, outputStream, backupStream, diag, context);
1937 }
1938}
1939
1940} // namespace TagParser
The AbortableProgressFeedback class provides feedback about an ongoing operation via callbacks.
bool isAborted() const
Returns whether the operation has been aborted via tryToAbort().
void parseHeader(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the header if not parsed yet.
const std::string & documentType() const
Returns a string that describes the document type if available; otherwise returns an empty string.
The AbstractTrack class parses and stores technical information about video, audio and other kinds of...
const Locale & locale() const
Returns the locale of the track if known; otherwise returns an empty locale.
MediaType mediaType() const
Returns the media type if known; otherwise returns MediaType::Other.
The BasicFileInfo class provides basic file information such as file name, extension,...
void reportPathChanged(std::string_view newPath)
Call this function to report that the path changed.
const std::string & path() const
Returns the path of the current file.
std::uint64_t size() const
Returns size of the current file in bytes.
virtual void invalidated()
This function is called when the BasicFileInfo gets invalidated.
CppUtilities::NativeFileStream & stream()
Returns the std::fstream for the current instance.
void open(bool readOnly=false)
Opens a std::fstream for the current file.
void close()
A possibly opened std::fstream will be closed.
static std::string_view pathForOpen(std::string_view url)
Returns removes the "file:/" prefix from url to be able to pass it to functions like open(),...
void reportSizeChanged(std::uint64_t newSize)
Call this function to report that the size changed.
void updateStep(const std::string &step, std::uint8_t stepPercentage=0)
Updates the current step and invokes the first callback specified on construction.
The Diagnostics class is a container for DiagMessage.
The class inherits from std::exception and serves as base class for exceptions thrown by the elements...
std::size_t insertFields(const FieldMapBasedTag< ImplementationType > &from, bool overwrite)
Inserts all fields from another tag of the same field type and compare function.
Implementation of TagParser::AbstractTrack for raw FLAC streams.
VorbisComment * createVorbisComment()
Creates a new Vorbis comment for the stream.
std::uint32_t paddingSize() const
Returns the padding size.
Definition flacstream.h:59
bool removeVorbisComment()
Removes the assigned Vorbis comment if one is assigned; does nothing otherwise.
VorbisComment * vorbisComment() const
Returns the Vorbis comment if one is present in the stream.
Definition flacstream.h:51
const std::vector< std::unique_ptr< TrackType > > & tracks() const
Returns the tracks of the file.
void validateElementStructure(Diagnostics &diag, AbortableProgressFeedback &progress, std::uint64_t *paddingSize=nullptr)
Parses all elements the file consists of.
const std::vector< std::unique_ptr< TagType > > & tags() const
Returns the tags of the file.
Implementation of TagParser::Tag for ID3v1 tags.
void ensureTextValuesAreProperlyEncoded() override
Ensures the encoding of all assigned text values is supported by the tag by converting the character ...
Definition id3v1tag.cpp:260
Implementation of TagParser::Tag for ID3v2 tags.
The exception that is thrown when the data to be parsed or to be made seems invalid and therefore can...
Implementation of GenericContainer<MediaFileInfo, MatroskaTag, MatroskaTrack, EbmlElement>.
The MediaFileInfo class allows to read and write tag information providing a container/tag format ind...
bool isForcingRewrite() const
Returns whether forcing rewriting (when applying changes) is enabled.
const std::vector< std::unique_ptr< Id3v2Tag > > & id3v2Tags() const
Returns pointers to the assigned ID3v2 tags.
void parseEverything(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the container format, the tracks and the tag information of the current file.
const std::vector< std::unique_ptr< MatroskaTag > > & matroskaTags() const
Returns pointers to the assigned Matroska tags.
std::string_view containerFormatAbbreviation() const
Returns the abbreviation of the container format as C-style string.
std::vector< AbstractTrack * > tracks() const
Returns the tracks for the current file.
bool areChaptersSupported() const
Returns an indication whether this library supports parsing the chapters of the current file.
ParsingStatus tagsParsingStatus() const
Returns an indication whether tag information has been parsed yet.
const std::string & saveFilePath() const
Returns the "save file path" which has been set using setSaveFilePath().
VorbisComment * createVorbisComment()
Creates a Vorbis comment for the current file.
VorbisComment * vorbisComment() const
Returns a pointer to the first assigned Vorbis comment or nullptr if none is assigned.
void removeAllTags()
Removes all assigned tags from the file.
~MediaFileInfo() override
Destroys the MediaFileInfo.
bool removeId3v1Tag()
Removes a possibly assigned ID3v1 tag from the current file.
bool hasTracksOfType(TagParser::MediaType type) const
Returns an indication whether the current file has tracks of the specified type.
void invalidated() override
Reimplemented from BasicFileInfo::invalidated().
std::size_t trackCount() const
Returns the number of tracks that could be parsed.
void parseAttachments(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the attachments of the current file.
void parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tracks of the current file.
std::vector< Tag * > tags() const
Returns all tags assigned to the current file.
bool removeId3v2Tag(Id3v2Tag *tag)
Removes an assigned ID3v2 tag from the current file.
bool areAttachmentsSupported() const
Returns an indication whether this library supports attachment format of the current file.
void parseContainerFormat(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the container format of the current file.
std::size_t maxPadding() const
Returns the maximum padding to be written before the data blocks when applying changes.
ParsingStatus tracksParsingStatus() const
Returns an indication whether tracks have been parsed yet.
Id3v1Tag * id3v1Tag() const
Returns a pointer to the assigned ID3v1 tag or nullptr if none is assigned.
std::string technicalSummary() const
Generates a short technical summary about the file's tracks.
CppUtilities::TimeSpan duration() const
Returns the overall duration of the file if known; otherwise returns a TimeSpan with zero ticks.
void parseChapters(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the chapters of the current file.
std::size_t preferredPadding() const
Returns the padding to be written before the data block when applying changes and the file needs to b...
bool removeVorbisComment()
Removes all assigned Vorbis comment from the current file.
void parseTags(Diagnostics &diag, AbortableProgressFeedback &progress)
Parses the tag(s) of the current file.
bool id3v2ToId3v1()
Converts the existing ID3v2 tags into an ID3v1 tag.
Id3v2Tag * createId3v2Tag()
Creates an ID3v2 tag for the current file.
bool areTracksSupported() const
Returns an indication whether this library supports parsing the tracks information of the current fil...
bool createAppropriateTags(const TagCreationSettings &settings=TagCreationSettings())
Ensures appropriate tags are created according the given settings.
void clearParsingResults()
Clears all parsing results and assigned/created/changed information such as detected container format...
std::size_t minPadding() const
Returns the minimum padding to be written before the data blocks when applying changes.
ParsingStatus attachmentsParsingStatus() const
Returns whether the attachments have been parsed yet.
Id3v1Tag * createId3v1Tag()
Creates an ID3v1 tag for the current file.
bool hasId3v2Tag() const
Returns an indication whether an ID3v2 tag is assigned.
bool isForcingFullParse() const
Returns an indication whether forcing a full parse is enabled.
std::uint64_t effectiveSize() const
Returns the "effective size" of the file if know; otherwise returns 0.
std::unordered_set< std::string > availableLanguages(TagParser::MediaType type=TagParser::MediaType::Audio) const
Determines the available languages for specified media type (by default MediaType::Audio).
bool areTagsSupported() const
Returns an indication whether this library supports the tag format of the current file.
bool removeAllId3v2Tags()
Removes all assigned ID3v2 tags from the current file.
AbstractContainer * container() const
Returns the container for the current file.
std::string_view mimeType() const
Returns the MIME-type of the container format as C-style string.
ContainerFormat containerFormat() const
Returns the container format of the current file.
std::vector< Tag * > parsedTags() const
Returns all tags parsed from the current file.
ParsingStatus containerParsingStatus() const
Returns an indication whether the container format has been parsed yet.
bool id3v1ToId3v2()
Converts an existing ID3v1 tag into an ID3v2 tag.
double overallAverageBitrate() const
Returns the overall average bitrate in kbit/s of the file if known; otherwise returns 0....
std::vector< AbstractChapter * > chapters() const
Returns all chapters assigned to the current file.
void applyChanges(Diagnostics &diag, AbortableProgressFeedback &progress)
Applies assigned/changed tag information to the current file.
std::uint64_t containerOffset() const
Returns the actual container start offset.
std::vector< AbstractAttachment * > attachments() const
Returns all attachments assigned to the current file.
static void writePadding(std::ostream &outputStream, uint64_t size)
Writes the specified number of zeroes to outputStream.
void mergeId3v2Tags()
Merges the assigned ID3v2 tags into a single ID3v2 tag.
bool hasId3v1Tag() const
Returns an indication whether an ID3v1 tag is assigned.
ParsingStatus chaptersParsingStatus() const
Returns whether the chapters have been parsed yet.
bool removeTag(Tag *tag)
Removes a possibly assigned tag from the current file.
MediaFileInfo()
Constructs a new MediaFileInfo.
bool hasAnyTag() const
Returns an indication whether a tag of any format is assigned.
const std::string & backupDirectory() const
Returns the directory used to store backup files.
Mp4Tag * mp4Tag() const
Returns a pointer to the assigned MP4 tag or nullptr if none is assigned.
Implementation of GenericContainer<MediaFileInfo, Mp4Tag, Mp4Track, Mp4Atom>.
Implementation of TagParser::Tag for the MP4 container.
The exception that is thrown when the data to be parsed holds no parsable information (e....
This exception is thrown when the an operation is invoked that has not been implemented yet.
Implementation of TagParser::AbstractContainer for Ogg files.
void setChecksumValidationEnabled(bool enabled)
Sets whether checksum validation is enabled.
std::size_t tagCount() const override
Returns the number of tags attached to the container.
void removeAllTags() override
Actually just flags all tags as removed and clears all assigned fields.
OggVorbisComment * createTag(const TagTarget &target) override
Creates a new tag.
The exception that is thrown when an operation has been stopped and thus not successfully completed b...
The TagTarget class specifies the target of a tag.
The Tag class is used to store, read and write tag information.
std::uint64_t size() const
Returns the size the tag within the file it is parsed from in bytes.
Definition tag.h:236
virtual std::size_t insertValues(const Tag &from, bool overwrite)
Inserts all compatible values from another Tag.
Definition tag.cpp:89
Implementation of TagParser::Tag for Vorbis comments.
TAG_PARSER_EXPORT void handleFailureAfterFileModifiedCanonical(MediaFileInfo &fileInfo, const std::string &originalPath, const std::string &backupPath, CppUtilities::NativeFileStream &outputStream, CppUtilities::NativeFileStream &backupStream, Diagnostics &diag, const std::string &context="making file")
Handles a failure/abort which occurred after the file has been modified.
TAG_PARSER_EXPORT void createBackupFileCanonical(const std::string &backupDir, std::string &originalPath, std::string &backupPath, CppUtilities::NativeFileStream &originalStream, CppUtilities::NativeFileStream &backupStream)
Creates a backup file like createBackupFile() but canonicalizes originalPath before doing the backup.
Contains all classes and functions of the TagInfo library.
Definition aaccodebook.h:10
ElementPosition
Definition settings.h:13
TAG_PARSER_EXPORT std::string_view containerFormatAbbreviation(ContainerFormat containerFormat, MediaType mediaType=MediaType::Unknown, unsigned int version=0)
Returns the abbreviation of the container format as C-style string considering the specified media ty...
TAG_PARSER_EXPORT ContainerFormat parseSignature(const char *buffer, std::size_t bufferSize)
Definition signature.h:83
TAG_PARSER_EXPORT std::string_view containerMimeType(ContainerFormat containerFormat, MediaType mediaType=MediaType::Unknown)
Returns the MIME-type of the container format as C-style string.
ParsingStatus
The ParsingStatus enum specifies whether a certain part of the file (tracks, tags,...
MediaType
The MediaType enum specifies the type of media data (audio, video, text, ...).
Definition mediaformat.h:14
MediaFileStructureFlags
The MediaFileStructureFlags enum specifies flags which describing the structure of a media file.
ContainerFormat
Specifies the container format.
Definition signature.h:18
MediaFileHandlingFlags
The MediaFileHandlingFlags enum specifies flags which controls the behavior of MediaFileInfo objects.
const LocaleDetail & someAbbreviatedName(LocaleFormat preferredFormat=LocaleFormat::BCP_47) const
Returns some abbreviated name, preferably of the specified preferredFormat.
The MediaFileInfoPrivate struct contains private fields of the MediaFileInfo class.
The TagSettings struct contains settings which can be passed to MediaFileInfo::createAppropriateTags(...
Definition settings.h:50
std::vector< TagTarget > requiredTargets
Specifies the required targets. If targets are not supported by the container an informal notificatio...
Definition settings.h:52
std::uint8_t id3v2MajorVersion
Specifies the ID3v2 version to be used in case an ID3v2 tag present or will be created....
Definition settings.h:61
TagUsage id3v2usage
Specifies the usage of ID3v2 when creating tags for MP3 files (has no effect when the file is no MP3 ...
Definition settings.h:59
TagCreationFlags flags
Specifies options to control the tag creation. See TagSettings::Flags.
Definition settings.h:54
TagUsage id3v1usage
Specifies the usage of ID3v1 when creating tags for MP3 files (has no effect when the file is no MP3 ...
Definition settings.h:57