#include "./mediafileinfo.h" #include "./abstracttrack.h" #include "./backuphelper.h" #include "./diagnostics.h" #include "./exceptions.h" #include "./locale.h" #include "./progressfeedback.h" #include "./signature.h" #include "./tag.h" #include "./id3/id3v1tag.h" #include "./id3/id3v2tag.h" #include "./wav/waveaudiostream.h" #include "./mpegaudio/mpegaudioframestream.h" #include "./adts/adtsstream.h" #include "./ivf/ivfstream.h" #include "./mp4/mp4atom.h" #include "./mp4/mp4container.h" #include "./mp4/mp4ids.h" #include "./mp4/mp4tag.h" #include "./mp4/mp4track.h" #include "./matroska/ebmlelement.h" #include "./matroska/matroskacontainer.h" #include "./matroska/matroskatag.h" #include "./matroska/matroskatrack.h" #include "./ogg/oggcontainer.h" #include "./flac/flacmetadata.h" #include "./flac/flacstream.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace std::placeholders; using namespace CppUtilities; /*! * \namespace IoUtilities * \brief Contains utility classes helping to read and write streams. */ 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 * a container/tag format independent interface. * * It also provides some technical information such as contained streams. * * For examples see "cli/mainfeatures.cpp" of the tageditor repository. */ /*! * \brief Constructs a new MediaFileInfo for the specified file. * * \param path Specifies the absolute or relative path of the file. */ MediaFileInfo::MediaFileInfo(std::string &&path) : BasicFileInfo(std::move(path)) , m_containerParsingStatus(ParsingStatus::NotParsedYet) , 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) , m_chaptersParsingStatus(ParsingStatus::NotParsedYet) , m_attachmentsParsingStatus(ParsingStatus::NotParsedYet) , m_minPadding(0) , m_maxPadding(0) , m_preferredPadding(0) , m_tagPosition(ElementPosition::BeforeData) , m_indexPosition(ElementPosition::BeforeData) , m_fileHandlingFlags(MediaFileHandlingFlags::ForceRewrite | MediaFileHandlingFlags::ForceTagPosition | MediaFileHandlingFlags::ForceIndexPosition | MediaFileHandlingFlags::NormalizeKnownTagFieldIds | MediaFileHandlingFlags::PreserveRawTimingValues) , m_maxFullParseSize(0x3200000) { } /*! * \brief Constructs a new MediaFileInfo. */ MediaFileInfo::MediaFileInfo() : MediaFileInfo(std::string()) { } /*! * \brief Constructs a new MediaFileInfo. */ MediaFileInfo::MediaFileInfo(std::string_view path) : MediaFileInfo(std::string(path)) { } /*! * \brief Destroys the MediaFileInfo. */ MediaFileInfo::~MediaFileInfo() { } /*! * \brief Parses the container format of the current file. * * This method parses the container of the current file format if it has not been * parsed yet. * * After calling this method the methods containerFormat(), containerFormatName(), * containerFormatAbbreviation(), containerFormatSubversion(), containerMimeType(), * container(), mp4Container() and matroskaContainer() will return the parsed * information. * \throws Throws std::ios_base::failure when an IO error occurs. * \sa isContainerParsed(), parseTracks(), parseTag(), parseChapters(), parseEverything() */ void MediaFileInfo::parseContainerFormat(Diagnostics &diag, AbortableProgressFeedback &progress) { CPP_UTILITIES_UNUSED(progress) // skip if container format already parsed if (containerParsingStatus() != ParsingStatus::NotParsedYet) { return; } static const string context("parsing file header"); open(); // ensure the file is open m_containerFormat = ContainerFormat::Unknown; // file size m_paddingSize = 0; m_containerOffset = 0; std::size_t bytesSkippedBeforeContainer = 0; std::streamoff id3v2Size = 0; // read signatrue char buff[16]; const char *const buffEnd = buff + sizeof(buff), *buffOffset; startParsingSignature: if (progress.isAborted()) { diag.emplace_back(DiagLevel::Information, "Parsing the container format has been aborted.", context); return; } if (size() - containerOffset() >= 16) { stream().seekg(m_containerOffset, ios_base::beg); stream().read(buff, sizeof(buff)); // skip zero/junk bytes // notes: // - Only skipping 4 or more consecutive zero bytes at this point because some signatures start with up to 4 zero bytes. // - It seems that most players/tools¹ skip junk bytes, at least when reading MP3 files. Hence the tagparser library is following // the same approach. (¹e.g. ffmpeg: "[mp3 @ 0x559e1f4cbd80] Skipping 1670 bytes of junk at 1165.") std::size_t bytesSkipped = 0; for (buffOffset = buff; buffOffset != buffEnd && !(*buffOffset); ++buffOffset, ++bytesSkipped) ; if (bytesSkipped >= 4) { skipJunkBytes: m_containerOffset += static_cast(bytesSkipped); m_paddingSize += bytesSkipped; // give up after 0x800 bytes if ((bytesSkippedBeforeContainer += bytesSkipped) >= 0x800u) { m_containerParsingStatus = ParsingStatus::NotSupported; m_containerFormat = ContainerFormat::Unknown; m_containerOffset = id3v2Size; return; } // try again goto startParsingSignature; } // parse signature switch ((m_containerFormat = parseSignature(buff, sizeof(buff)))) { case ContainerFormat::Id3v2Tag: // save position of ID3v2 tag m_actualId3v2TagOffsets.push_back(m_containerOffset); if (m_actualId3v2TagOffsets.size() == 2) { diag.emplace_back(DiagLevel::Warning, "There is more than just one ID3v2 header at the beginning of the file.", context); } // read ID3v2 header stream().seekg(m_containerOffset + 5, ios_base::beg); stream().read(buff, 5); // set the container offset to skip ID3v2 header m_containerOffset += toNormalInt(BE::toInt(buff + 1)) + 10; if ((*buff) & 0x10) { // footer present m_containerOffset += 10; } id3v2Size = m_containerOffset; // continue reading signature goto startParsingSignature; case ContainerFormat::Mp4: case ContainerFormat::QuickTime: { // MP4/QuickTime is handled using Mp4Container instance m_container = make_unique(*this, m_containerOffset); try { static_cast(m_container.get())->validateElementStructure(diag, progress, &m_paddingSize); } catch (const OperationAbortedException &) { diag.emplace_back(DiagLevel::Information, "Validating the MP4 element structure has been aborted.", context); } catch (const Failure &) { m_containerParsingStatus = ParsingStatus::CriticalFailure; } break; } case ContainerFormat::Ebml: { // EBML/Matroska is handled using MatroskaContainer instance auto container = make_unique(*this, m_containerOffset); try { container->parseHeader(diag, progress); if (container->documentType() == "matroska") { m_containerFormat = ContainerFormat::Matroska; } else if (container->documentType() == "webm") { m_containerFormat = ContainerFormat::Webm; } if (isForcingFullParse()) { // validating the element structure of Matroska files takes too long when // parsing big files so do this only when explicitly desired container->validateElementStructure(diag, progress, &m_paddingSize); container->validateIndex(diag, progress); } } catch (const OperationAbortedException &) { diag.emplace_back(DiagLevel::Information, "Validating the Matroska element structure has been aborted.", context); } catch (const Failure &) { m_containerParsingStatus = ParsingStatus::CriticalFailure; } m_container = std::move(container); break; } case ContainerFormat::Ogg: // Ogg is handled by OggContainer instance m_container = make_unique(*this, m_containerOffset); static_cast(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(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) { stream().seekg(0x101); stream().read(buff, 6); if (buff[0] == 0x75 && buff[1] == 0x73 && buff[2] == 0x74 && buff[3] == 0x61 && buff[4] == 0x72 && buff[5] == 0x00) { m_containerFormat = ContainerFormat::Tar; break; } } // skip previously determined zero-bytes or try our luck on the next byte if (!bytesSkipped) { ++bytesSkipped; } goto skipJunkBytes; default:; } } if (bytesSkippedBeforeContainer) { diag.emplace_back(DiagLevel::Warning, argsToString(bytesSkippedBeforeContainer, " bytes of junk skipped"), context); } // set parsing status if (m_containerParsingStatus == ParsingStatus::NotParsedYet) { if (m_containerFormat == ContainerFormat::Unknown) { m_containerParsingStatus = ParsingStatus::NotSupported; } else { m_containerParsingStatus = ParsingStatus::Ok; } } } /*! * \brief Parses the tracks of the current file. * * This method parses the tracks of the current file if not been parsed yet. * After calling this method the methods trackCount(), tracks(), and * hasTracksOfType() will return the parsed information. * * \throws Throws std::ios_base::failure when an IO error occurs. * \remarks parseContainerFormat() must be called before. * \sa areTracksParsed(), parseContainerFormat(), parseTags(), parseChapters(), parseEverything() */ void MediaFileInfo::parseTracks(Diagnostics &diag, AbortableProgressFeedback &progress) { // skip if tracks already parsed if (tracksParsingStatus() != ParsingStatus::NotParsedYet) { return; } static const string context("parsing tracks"); try { // parse tracks via container object if (m_container) { m_container->parseTracks(diag, progress); m_tracksParsingStatus = ParsingStatus::Ok; return; } // parse tracks via track object for "single-track"-formats switch (m_containerFormat) { case ContainerFormat::Adts: m_singleTrack = make_unique(stream(), m_containerOffset); break; case ContainerFormat::Flac: m_singleTrack = make_unique(*this, m_containerOffset); break; case ContainerFormat::Ivf: m_singleTrack = make_unique(stream(), m_containerOffset); break; case ContainerFormat::MpegAudioFrames: m_singleTrack = make_unique(stream(), m_containerOffset); break; case ContainerFormat::RiffWave: m_singleTrack = make_unique(stream(), m_containerOffset); break; 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 switch (m_containerFormat) { case ContainerFormat::Flac: m_paddingSize += static_cast(m_singleTrack.get())->paddingSize(); break; default:; } m_tracksParsingStatus = ParsingStatus::Ok; } catch (const NotImplementedException &) { diag.emplace_back(DiagLevel::Information, "Parsing tracks is not implemented for the container format of the file.", context); m_tracksParsingStatus = ParsingStatus::NotSupported; } catch (const OperationAbortedException &) { diag.emplace_back(DiagLevel::Information, "Parsing tracks has been aborted.", context); } catch (const Failure &) { diag.emplace_back(DiagLevel::Critical, "Unable to parse tracks.", context); m_tracksParsingStatus = ParsingStatus::CriticalFailure; } } /*! * \brief Parses the tag(s) of the current file. * * This method parses the tag(s) of the current file if not been parsed yet. * After calling this method the methods id3v1Tag(), id3v2Tags(), * mp4Tag() and allTags() will return the parsed information. * * Previously assigned but not applied tag information will be discarted. * \throws Throws std::ios_base::failure when an IO error occurs. * \remarks parseContainerFormat() must be called before. * \sa isTagParsed(), parseContainerFormat(), parseTracks(), parseChapters(), parseEverything() */ void MediaFileInfo::parseTags(Diagnostics &diag, AbortableProgressFeedback &progress) { // skip if tags already parsed if (tagsParsingStatus() != ParsingStatus::NotParsedYet) { return; } static const string context("parsing tag"); // check for ID3v1 tag auto effectiveSize = static_cast(size()); if (effectiveSize >= 128) { m_id3v1Tag = make_unique(); try { 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 &) { diag.emplace_back(DiagLevel::Information, "Parsing ID3v1 tag has been aborted.", context); return; } catch (const Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v1 tag.", context); } } // 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(buffer) == 0x4150455441474558ul /* APETAGEX */) { // take record of APE tag const auto tagSize = static_cast(LE::toInt(buffer + 12)); const auto flags = LE::toInt(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) { auto id3v2Tag = make_unique(); stream().seekg(offset, ios_base::beg); try { id3v2Tag->parse(stream(), size() - static_cast(offset), diag); m_paddingSize += id3v2Tag->paddingSize(); } catch (const NoDataFoundException &) { continue; } catch (const OperationAbortedException &) { diag.emplace_back(DiagLevel::Information, "Parsing ID3v2 tags has been aborted.", context); return; } catch (const Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; diag.emplace_back(DiagLevel::Critical, "Unable to parse ID3v2 tag.", context); } m_id3v2Tags.emplace_back(id3v2Tag.release()); } // compute effective size m_effectiveSize = static_cast(effectiveSize - m_containerOffset); // check for tags in tracks (FLAC only) or via container object try { if (m_containerFormat == ContainerFormat::Flac) { parseTracks(diag, progress); if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) { m_tagsParsingStatus = m_tracksParsingStatus; } return; } else if (m_container) { m_container->parseTags(diag, progress); } else if (m_containerFormat != ContainerFormat::MpegAudioFrames) { throw NotImplementedException(); } // set status, but do not override error/unsupported status form ID3 tags here if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) { m_tagsParsingStatus = ParsingStatus::Ok; } } catch (const NotImplementedException &) { // set status to not supported, but do not override parsing status from ID3 tags here if (m_tagsParsingStatus == ParsingStatus::NotParsedYet) { m_tagsParsingStatus = ParsingStatus::NotSupported; } diag.emplace_back(DiagLevel::Information, "Parsing tags is not implemented for the container format of the file.", context); } catch (const OperationAbortedException &) { diag.emplace_back(DiagLevel::Information, "Parsing tags from container/streams has been aborted.", context); return; } catch (const Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; diag.emplace_back(DiagLevel::Critical, "Unable to parse tag.", context); } } /*! * \brief Parses the chapters of the current file. * * This method parses the chapters of the current file if not been parsed yet. * * \throws Throws std::ios_base::failure when an IO error occurs. * \remarks parseContainerFormat() must be called before. * \sa areChaptersParsed(), parseContainerFormat(), parseTracks(), parseTags(), parseEverything() */ void MediaFileInfo::parseChapters(Diagnostics &diag, AbortableProgressFeedback &progress) { // skip if chapters already parsed if (chaptersParsingStatus() != ParsingStatus::NotParsedYet) { return; } static const string context("parsing chapters"); try { // parse chapters via container object if (!m_container) { throw NotImplementedException(); } m_container->parseChapters(diag, progress); m_chaptersParsingStatus = ParsingStatus::Ok; } catch (const NotImplementedException &) { m_chaptersParsingStatus = ParsingStatus::NotSupported; diag.emplace_back(DiagLevel::Information, "Parsing chapters is not implemented for the container format of the file.", context); } catch (const Failure &) { m_chaptersParsingStatus = ParsingStatus::CriticalFailure; diag.emplace_back(DiagLevel::Critical, "Unable to parse chapters.", context); } } /*! * \brief Parses the attachments of the current file. * * This method parses the attachments of the current file if not been parsed yet. * * \throws Throws std::ios_base::failure when an IO error occurs. * \remarks parseContainerFormat() must be called before. * \sa areChaptersParsed(), parseContainerFormat(), parseTracks(), parseTags(), parseEverything() */ void MediaFileInfo::parseAttachments(Diagnostics &diag, AbortableProgressFeedback &progress) { // skip if attachments already parsed if (attachmentsParsingStatus() != ParsingStatus::NotParsedYet) { return; } static const string context("parsing attachments"); try { // parse attachments via container object if (!m_container) { throw NotImplementedException(); } m_container->parseAttachments(diag, progress); m_attachmentsParsingStatus = ParsingStatus::Ok; } catch (const NotImplementedException &) { m_attachmentsParsingStatus = ParsingStatus::NotSupported; diag.emplace_back(DiagLevel::Information, "Parsing attachments is not implemented for the container format of the file.", context); } catch (const Failure &) { m_attachmentsParsingStatus = ParsingStatus::CriticalFailure; diag.emplace_back(DiagLevel::Critical, "Unable to parse attachments.", context); } } /*! * \brief Parses the container format, the tracks and the tag information of the current file. * * See the individual methods to for more details and exceptions which might be thrown. * \sa parseContainerFormat(), parseTracks(), parseTags() */ void MediaFileInfo::parseEverything(Diagnostics &diag, AbortableProgressFeedback &progress) { parseContainerFormat(diag, progress); if (progress.isAborted()) { return; } parseTracks(diag, progress); if (progress.isAborted()) { return; } parseTags(diag, progress); if (progress.isAborted()) { return; } parseChapters(diag, progress); if (progress.isAborted()) { return; } parseAttachments(diag, progress); } /*! * \brief Ensures appropriate tags are created according the given \a settings. * \return Returns whether appropriate tags could be created for the file. * \remarks * - Tags must have been parsed before invoking this method (otherwise it will just return false). * - The ID3 related arguments are only practiced when the file format is MP3 or when the file format is unknown and \a treatUnknownFilesAsMp3Files is true. * - Tags might be removed as well. For example the existing ID3v1 tag of an MP3 file will be removed if \a id3v1Usage is set to TagUsage::Never. * - The method might do nothing if present tag(s) already match the given specifications. * - This is only a convenience method. The task could be done by manually using the methods createId3v1Tag(), createId3v2Tag(), removeId3v1Tag() ... as well. * - Some tag information might be discarded. For example when an ID3v2 tag needs to be removed (TagSettings::id3v2usage is set to TagUsage::Never) and an ID3v1 tag will be created instead not all fields can be transferred. */ bool MediaFileInfo::createAppropriateTags(const TagCreationSettings &settings) { // check if tags have been parsed yet (tags must have been parsed yet to create appropriate tags) if (tagsParsingStatus() == ParsingStatus::NotParsedYet) { return false; } // check if tags need to be created/adjusted/removed const auto requiredTargets(settings.requiredTargets); const auto flags(settings.flags); const auto targetsRequired = !requiredTargets.empty() && (requiredTargets.size() != 1 || !requiredTargets.front().isEmpty()); auto targetsSupported = false; if (areTagsSupported() && m_container) { // container object takes care of tag management if (targetsRequired) { // check whether container supports targets if (m_container->tagCount()) { // all tags in the container should support targets if the first one supports targets targetsSupported = m_container->tag(0)->supportsTarget(); } else { // try to create a new tag and check whether targets are supported auto *const tag = m_container->createTag(); if (tag && (targetsSupported = tag->supportsTarget())) { tag->setTarget(requiredTargets.front()); } } if (targetsSupported) { for (const auto &target : requiredTargets) { m_container->createTag(target); } } } else { // no targets are required -> just ensure that at least one tag is present m_container->createTag(); } return true; } // no container object present switch (m_containerFormat) { case ContainerFormat::Flac: static_cast(m_singleTrack.get())->createVorbisComment(); break; default: // create ID3 tag(s) if (!hasAnyTag() && !(flags & TagCreationFlags::TreatUnknownFilesAsMp3Files)) { switch (containerFormat()) { case ContainerFormat::Adts: case ContainerFormat::Aiff: case ContainerFormat::MpegAudioFrames: case ContainerFormat::WavPack: break; default: return false; } } // create ID3 tags according to id3v2usage and id3v2usage // always create ID3v1 tag -> ensure there is one if (settings.id3v1usage == TagUsage::Always && !id3v1Tag()) { auto *const id3v1Tag = createId3v1Tag(); if (flags & TagCreationFlags::Id3InitOnCreate) { for (const auto &id3v2Tag : id3v2Tags()) { // overwrite existing values to ensure default ID3v1 genre "Blues" is updated as well id3v1Tag->insertValues(*id3v2Tag, true); // ID3v1 does not support all text encodings which might be used in ID3v2 id3v1Tag->ensureTextValuesAreProperlyEncoded(); } } } if (settings.id3v2usage == TagUsage::Always && !hasId3v2Tag()) { // always create ID3v2 tag -> ensure there is one and set version auto *const id3v2Tag = createId3v2Tag(); id3v2Tag->setVersion(settings.id3v2MajorVersion, 0); if ((flags & TagCreationFlags::Id3InitOnCreate) && id3v1Tag()) { id3v2Tag->insertValues(*id3v1Tag(), true); } } } if (flags & TagCreationFlags::MergeMultipleSuccessiveId3v2Tags) { mergeId3v2Tags(); } // remove ID3 tags according to settings if (settings.id3v1usage == TagUsage::Never && hasId3v1Tag()) { // transfer tags to ID3v2 tag before removing if ((flags & TagCreationFlags::Id3TransferValuesOnRemoval) && hasId3v2Tag()) { id3v2Tags().front()->insertValues(*id3v1Tag(), false); } removeId3v1Tag(); } if (settings.id3v2usage == TagUsage::Never) { if ((flags & TagCreationFlags::Id3TransferValuesOnRemoval) && hasId3v1Tag()) { // transfer tags to ID3v1 tag before removing for (const auto &tag : id3v2Tags()) { id3v1Tag()->insertValues(*tag, false); } } removeAllId3v2Tags(); } else if (!(flags & TagCreationFlags::KeepExistingId3v2Version)) { // set version of ID3v2 tag according user preferences for (const auto &tag : id3v2Tags()) { tag->setVersion(settings.id3v2MajorVersion, 0); } } return true; } /*! * \brief Applies assigned/changed tag information to the current file. * * This method applies previously assigned tag information to the current file. * * Depending on the changes to be applied the file will be rewritten. * * When the file needs to be rewritten it will be renamed. A new file with the old name * will be created to replace the old file. * * \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws TagParser::Failure or a derived exception when a making error occurs. * * \remarks Tags and tracks need to be parsed without errors before this method can be called. * All previous parsing results are cleared (using clearParsingResults()). Hence * the file must be reparsed. All related objects (tags, tracks, ...) might get invalidated. * This includes notifications of these objects as well. * * \sa clearParsingResults() */ void MediaFileInfo::applyChanges(Diagnostics &diag, AbortableProgressFeedback &progress) { static const string context("making file"); diag.emplace_back(DiagLevel::Information, "Changes are about to be applied.", context); bool previousParsingSuccessful = true; switch (tagsParsingStatus()) { case ParsingStatus::Ok: case ParsingStatus::NotSupported: break; default: previousParsingSuccessful = false; diag.emplace_back(DiagLevel::Critical, "Tags have to be parsed without critical errors before changes can be applied.", context); } switch (tracksParsingStatus()) { case ParsingStatus::Ok: case ParsingStatus::NotSupported: break; default: previousParsingSuccessful = false; diag.emplace_back(DiagLevel::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context); } if (!previousParsingSuccessful) { throw InvalidDataException(); } if (m_container) { // container object takes care // ID3 tags can not be applied in this case -> add warnings if ID3 tags have been assigned if (hasId3v1Tag()) { diag.emplace_back(DiagLevel::Warning, "Assigned ID3v1 tag can't be attached and will be ignored.", context); } if (hasId3v2Tag()) { diag.emplace_back(DiagLevel::Warning, "Assigned ID3v2 tag can't be attached and will be ignored.", context); } m_tracksParsingStatus = ParsingStatus::NotParsedYet; m_tagsParsingStatus = ParsingStatus::NotParsedYet; try { m_container->makeFile(diag, progress); } catch (...) { // since the file might be messed up, invalidate the parsing results clearParsingResults(); throw; } } else { // implementation if no container object is present // assume the file is a MP3 file try { makeMp3File(diag, progress); } catch (...) { // since the file might be messed up, invalidate the parsing results clearParsingResults(); throw; } } clearParsingResults(); } /*! * \brief Returns the abbreviation of the container format as C-style string. * * This abbreviation might be used as file extension. * * parseContainerFormat() needs to be called before. Otherwise * always an empty string will be returned. * * \sa containerFormat() * \sa containerFormatName() * \sa parseContainerFormat() */ string_view MediaFileInfo::containerFormatAbbreviation() const { MediaType mediaType = MediaType::Unknown; unsigned int version = 0; switch (m_containerFormat) { case ContainerFormat::Ogg: { // check for video track or whether only Opus or Speex tracks are present const auto &tracks = static_cast(m_container.get())->tracks(); if (tracks.empty()) { break; } bool onlyOpus = true, onlySpeex = true; for (const auto &track : static_cast(m_container.get())->tracks()) { if (track->mediaType() == MediaType::Video) { mediaType = MediaType::Video; } if (track->format().general != GeneralMediaFormat::Opus) { onlyOpus = false; } if (track->format().general != GeneralMediaFormat::Speex) { onlySpeex = false; } } if (onlyOpus) { version = static_cast(GeneralMediaFormat::Opus); } else if (onlySpeex) { version = static_cast(GeneralMediaFormat::Speex); } break; } case ContainerFormat::Matroska: case ContainerFormat::Mp4: mediaType = hasTracksOfType(MediaType::Video) ? MediaType::Video : MediaType::Audio; break; case ContainerFormat::MpegAudioFrames: if (m_singleTrack) { version = m_singleTrack->format().sub; } break; default:; } return TagParser::containerFormatAbbreviation(m_containerFormat, mediaType, version); } /*! * \brief Returns the MIME-type of the container format as C-style string. * * parseContainerFormat() needs to be called before. Otherwise * always an empty string will be returned. * * \sa containerFormat() * \sa containerFormatName() * \sa parseContainerFormat() */ string_view MediaFileInfo::mimeType() const { MediaType mediaType; switch (m_containerFormat) { case ContainerFormat::Mp4: case ContainerFormat::Ogg: case ContainerFormat::Matroska: mediaType = hasTracksOfType(MediaType::Video) ? MediaType::Video : MediaType::Audio; break; default: mediaType = MediaType::Unknown; } return TagParser::containerMimeType(m_containerFormat, mediaType); } /*! * \brief Returns the tracks for the current file. * * parseTracks() needs to be called before. Otherwise this * method always returns an empty vector. * * \remarks The MediaFileInfo keeps the ownership over the returned * pointers. The returned Tracks will be destroyed when the * MediaFileInfo is invalidated. * * \sa parseTracks() */ vector MediaFileInfo::tracks() const { vector res; size_t trackCount = 0; size_t containerTrackCount = 0; if (m_singleTrack) { trackCount = 1; } if (m_container) { trackCount += (containerTrackCount = m_container->trackCount()); } res.reserve(trackCount); if (m_singleTrack) { res.push_back(m_singleTrack.get()); } for (size_t i = 0; i != containerTrackCount; ++i) { res.push_back(m_container->track(i)); } return res; } /*! * \brief Returns an indication whether the current file has tracks of the specified \a type. * * parseTracks() needs to be called before. Otherwise this * method always returns false. * * \sa parseTracks() */ bool MediaFileInfo::hasTracksOfType(MediaType type) const { if (tracksParsingStatus() == ParsingStatus::NotParsedYet) { return false; } if (m_singleTrack && m_singleTrack->mediaType() == type) { return true; } else if (m_container) { for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) { if (m_container->track(i)->mediaType() == type) { return true; } } } return false; } /*! * \brief Returns the overall duration of the file if known; otherwise * returns a TimeSpan with zero ticks. * * parseTracks() needs to be called before. Otherwise this * method always returns false. * * \sa parseTracks() */ CppUtilities::TimeSpan MediaFileInfo::duration() const { if (m_container) { return m_container->duration(); } else if (m_singleTrack) { return m_singleTrack->duration(); } return TimeSpan(); } /*! * \brief Returns the overall average bitrate in kbit/s of the file if known; otherwise * returns 0.0. * * parseTracks() needs to be called before. Otherwise this * method always returns false. * * \sa parseTracks() */ double MediaFileInfo::overallAverageBitrate() const { const auto duration = this->duration(); if (duration.isNull()) { return 0.0; } return 0.0078125 * static_cast(size()) / duration.totalSeconds(); } /*! * \brief Determines the available languages for specified media type (by default MediaType::Audio). * * If \a type is MediaType::Unknown, all media types are considered. * * parseTracks() needs to be called before. Otherwise this * method always returns an empty set. * * \sa parseTracks() */ unordered_set MediaFileInfo::availableLanguages(MediaType type) const { unordered_set res; if (m_container) { for (size_t i = 0, count = m_container->trackCount(); i != count; ++i) { const AbstractTrack *const track = m_container->track(i); if (type != MediaType::Unknown && track->mediaType() != type) { continue; } if (const auto &language = track->locale().someAbbreviatedName(); !language.empty()) { res.emplace(language); } } } else if (m_singleTrack && (type == MediaType::Unknown || m_singleTrack->mediaType() == type)) { if (const auto &language = m_singleTrack->locale().someAbbreviatedName(); !language.empty()) { res.emplace(language); } } return res; } /*! * \brief Generates a short technical summary about the file's tracks. * * parseTracks() needs to be called before. Otherwise this * method always returns an empty string. * * Example (exact format might change in the future!): * "H.264-720p / HE-AAC-6ch-eng / HE-AAC-2ch-ger / SRT-eng / SRT-ger" * * \sa parseTracks() */ string MediaFileInfo::technicalSummary() const { if (m_container) { const size_t trackCount = m_container->trackCount(); vector parts; parts.reserve(trackCount); for (size_t i = 0; i != trackCount; ++i) { const string description(m_container->track(i)->description()); if (!description.empty()) { parts.emplace_back(std::move(description)); } } return joinStrings(parts, " / "); } else if (m_singleTrack) { return m_singleTrack->description(); } return string(); } /*! * \brief Removes a possibly assigned ID3v1 tag from the current file. * * To apply the removal and other changings call the applyChanges() method. * * \returns Returns whether there was an ID3v1 tag assigned which could be removed. * \remarks Invalidates the removed tag object (eg. returned via tags() or id3v1Tag()). * \sa applyChanges() */ bool MediaFileInfo::removeId3v1Tag() { if (tagsParsingStatus() == ParsingStatus::NotParsedYet) { return false; } if (m_id3v1Tag) { m_id3v1Tag.reset(); return true; } return false; } /*! * \brief Creates an ID3v1 tag for the current file. * * This method does nothing the tags of the current file haven't been parsed using * the parseTags() method. * * If the file has already an ID3v1 tag no new tag will be created. * * To apply the created tag and other changings call the applyChanges() method. * * \returns Returns the ID3v1 tag of the current file or nullptr if the tag haven't been * parsed yet. * * \sa applyChanges() */ Id3v1Tag *MediaFileInfo::createId3v1Tag() { if (tagsParsingStatus() == ParsingStatus::NotParsedYet) { return nullptr; } if (!m_id3v1Tag) { m_id3v1Tag = make_unique(); } return m_id3v1Tag.get(); } /*! * \brief Removes an assigned ID3v2 tag from the current file. * * To apply the removal and other changings call the applyChanges() method. * * \param tag Specifies the ID3v2 tag to be removed. * \returns Returns whether there the an ID3v2 tag could be removed. * \remarks Invalidates all removed tag objects (eg. returned via tags() or id3v2Tags()). * \sa applyChanges() */ bool MediaFileInfo::removeId3v2Tag(Id3v2Tag *tag) { if (tagsParsingStatus() == ParsingStatus::NotParsedYet) { return false; } for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) { if (i->get() == tag) { m_id3v2Tags.erase(i); return true; } } return false; } /*! * \brief Removes all assigned ID3v2 tags from the current file. * * To apply the removal and other changings call the applyChanges() method * \returns Returns whether there where ID3v2 tags assigned which could be removed. * \remarks Invalidates all removed tag objects (eg. returned via tags() or id3v2Tags()). * \sa applyChanges() */ bool MediaFileInfo::removeAllId3v2Tags() { if (tagsParsingStatus() == ParsingStatus::NotParsedYet || m_id3v2Tags.empty()) { return false; } m_id3v2Tags.clear(); return true; } /*! * \brief Creates an ID3v2 tag for the current file. * * This method does nothing the tags of the current file haven't been parsed using * the parseTags() method. * * If the file has already an ID3v2 tag no new tag will be created. * * To apply the created tag and other changings call the applyChanges() method. * * \returns Returns the first ID3v2 tag of the current file. * \remarks The MediaFileInfo keeps the ownership over the created tag. It will be * destroyed when the MediaFileInfo is invalidated. * \sa applyChanges() */ Id3v2Tag *MediaFileInfo::createId3v2Tag() { if (m_id3v2Tags.empty()) { m_id3v2Tags.emplace_back(make_unique()); } return m_id3v2Tags.front().get(); } /*! * \brief Removes a possibly assigned \a tag from the current file. * * To apply the removal and other changings call the applyChanges() method. * * \param tag Specifies the tag to be removed. The tag will not only be detached from the * file, it will be destroyed as well. Might be nullptr for convenience (eg. * you might want to call file.removeTag(file.mp4Tag()) to ensure no MP4 tag * is present without checking before). * \remarks Invalidates the removed tag object if it has been removed. * * \sa applyChanges() */ bool MediaFileInfo::removeTag(Tag *tag) { if (!tag) { return false; } // remove tag via container if (m_container) { return m_container->removeTag(tag); } // remove tag via track for "single-track" formats if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) { auto *const flacStream(static_cast(m_singleTrack.get())); if (flacStream->vorbisComment() == tag) { return flacStream->removeVorbisComment(); } } // remove ID3 tags if (m_id3v1Tag.get() == tag) { m_id3v1Tag.reset(); return true; } for (auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) { if (i->get() == tag) { m_id3v2Tags.erase(i); return true; } } return false; } /*! * \brief Removes all assigned tags from the file. * \remarks Invalidates all removed tag objects (eg. returned via tags()). * * To apply the removal and other changings call the applyChanges() method. */ void MediaFileInfo::removeAllTags() { if (m_container) { m_container->removeAllTags(); } if (m_singleTrack && m_containerFormat == ContainerFormat::Flac) { static_cast(m_singleTrack.get())->removeVorbisComment(); } m_id3v1Tag.reset(); m_id3v2Tags.clear(); } /*! * \brief Returns an indication whether this library supports parsing the chapters of the current file. */ bool MediaFileInfo::areChaptersSupported() const { if (m_container && m_container->chapterCount()) { return true; } switch (m_containerFormat) { case ContainerFormat::Matroska: case ContainerFormat::Webm: return true; default: return false; } } /*! * \brief Returns an indication whether this library supports attachment format of the current file. */ bool MediaFileInfo::areAttachmentsSupported() const { if (m_container && m_container->attachmentCount()) { return true; } switch (m_containerFormat) { case ContainerFormat::Matroska: case ContainerFormat::Webm: return true; default: return false; } } /*! * \brief Returns an indication whether this library supports parsing the tracks information of the current file. */ bool MediaFileInfo::areTracksSupported() const { if (trackCount()) { return true; } switch (m_containerFormat) { case ContainerFormat::Mp4: case ContainerFormat::MpegAudioFrames: case ContainerFormat::RiffWave: case ContainerFormat::Ogg: case ContainerFormat::Matroska: case ContainerFormat::Webm: return true; default: return false; } } /*! * \brief Returns an indication whether this library supports the tag format of the current file. */ bool MediaFileInfo::areTagsSupported() const { switch (m_containerFormat) { case ContainerFormat::Adts: case ContainerFormat::Flac: case ContainerFormat::Matroska: case ContainerFormat::MpegAudioFrames: case ContainerFormat::Mp4: case ContainerFormat::Ogg: case ContainerFormat::WavPack: case ContainerFormat::Webm: // these container formats are supported return true; default: // the container format is unsupported // -> an ID3 tag might be already present, in this case the tags are considered supported return !m_container && (hasId3v1Tag() || hasId3v2Tag()); } } /*! * \brief Returns a pointer to the assigned MP4 tag or nullptr if none is assigned. * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the * MediaFileInfo is invalidated. */ Mp4Tag *MediaFileInfo::mp4Tag() const { // simply return the first tag here since MP4 files never contain multiple tags return (m_containerFormat == ContainerFormat::Mp4 || m_containerFormat == ContainerFormat::QuickTime) && m_container && m_container->tagCount() > 0 ? static_cast(m_container.get())->tags().front().get() : nullptr; } /*! * \brief Returns pointers to the assigned Matroska tags. * \remarks The MediaFileInfo keeps the ownership over the returned * pointers. The returned Matroska tags will be destroyed when the * MediaFileInfo is invalidated. */ const vector> &MediaFileInfo::matroskaTags() const { // matroska files might contain multiple tags (targeting different scopes) if (m_containerFormat == ContainerFormat::Matroska && m_container) { return static_cast(m_container.get())->tags(); } else { static const std::vector> empty; return empty; } } /*! * \brief Returns a pointer to the first assigned Vorbis comment or nullptr if none is assigned. * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the * MediaFileInfo is invalidated. */ VorbisComment *MediaFileInfo::vorbisComment() const { return m_containerFormat == ContainerFormat::Ogg && m_container && m_container->tagCount() ? static_cast(m_container.get())->tags().front().get() : (m_containerFormat == ContainerFormat::Flac && m_singleTrack ? static_cast(m_singleTrack.get())->vorbisComment() : nullptr); } /*! * \brief Returns all chapters assigned to the current file. * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the * MediaFileInfo is invalidated. */ vector MediaFileInfo::chapters() const { vector res; if (m_container) { const size_t count = m_container->chapterCount(); res.reserve(count); for (size_t i = 0; i != count; ++i) { res.push_back(m_container->chapter(i)); } } return res; } /*! * \brief Returns all attachments assigned to the current file. * \remarks The MediaFileInfo keeps the ownership over the object which will be destroyed when the * MediaFileInfo is invalidated. */ vector MediaFileInfo::attachments() const { vector res; if (m_container) { const size_t count = m_container->attachmentCount(); res.reserve(count); for (size_t i = 0; i != count; ++i) { res.push_back(m_container->attachment(i)); } } return res; } /*! * \brief Clears all parsing results and assigned/created/changed information such as * detected container format, tracks, tags, ... * * This allows a rescan of the file using parsing methods like parseContainerFormat(). * Otherwise, these methods do nothing if the information to be parsed has already been * gathered. * * \remarks Any pointers previously returned by tags(), tracks(), ... object should be * considered invalidated. */ void MediaFileInfo::clearParsingResults() { m_containerParsingStatus = ParsingStatus::NotParsedYet; m_containerFormat = ContainerFormat::Unknown; m_containerOffset = 0; m_paddingSize = 0; m_tracksParsingStatus = ParsingStatus::NotParsedYet; m_tagsParsingStatus = ParsingStatus::NotParsedYet; m_chaptersParsingStatus = ParsingStatus::NotParsedYet; m_attachmentsParsingStatus = ParsingStatus::NotParsedYet; m_id3v1Tag.reset(); m_id3v2Tags.clear(); m_actualId3v2TagOffsets.clear(); m_fileStructureFlags = MediaFileStructureFlags::None; m_container.reset(); m_singleTrack.reset(); } /*! * \brief Merges the assigned ID3v2 tags into a single ID3v2 tag. * * Some files I've got contain multiple successive ID3v2 tags. If the tags of * such an file is parsed by this class, these tags will be kept separate. * This method merges all assigned ID3v2 tags. All fields from the additional * ID3v2 tags will be inserted to the first tag. All assigned ID3v2 tag instances * except thefirst will be destroyed. * * A possibly assigned ID3v1 tag remains unaffected. * * This method does nothing the tags of the current file haven't been parsed using * the parseTags() method. * * \sa id3v2Tags() */ void MediaFileInfo::mergeId3v2Tags() { auto begin = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); if (begin == end) { return; } Id3v2Tag &first = **begin; auto isecond = begin + 1; if (isecond == end) { return; } for (auto i = isecond; i != end; ++i) { first.insertFields(**i, false); } m_id3v2Tags.erase(isecond, end); } /*! * \brief Converts an existing ID3v1 tag into an ID3v2 tag. * * Effectively merges all ID3 tags into a single ID3v2 tag. * * \remarks Does nothing if * - there is no ID3v1 tag assigned. * - the file format isn't known to support ID3 tags (unless there is an ID3 tag present). * - the tags of the current file haven't been parsed using the parseTags() method. */ bool MediaFileInfo::id3v1ToId3v2() { if (tagsParsingStatus() == ParsingStatus::NotParsedYet || !areTagsSupported() || !hasId3v1Tag()) { return false; } return createAppropriateTags(TagCreationSettings{ {}, TagCreationFlags::MergeMultipleSuccessiveId3v2Tags | TagCreationFlags::KeepExistingId3v2Version, TagUsage::Never, TagUsage::Always, 3 }); } /*! * \brief Converts the existing ID3v2 tags into an ID3v1 tag. * * Effectively merges all ID3 tags into a single ID3v1 tag. * * \remarks Does nothing if * - there is not at least one ID3v2 tag assigned. * - the file format isn't known to support ID3 tags (unless there is an ID3 tag present). * - the tags of the current file haven't been parsed using the parseTags() method. */ bool MediaFileInfo::id3v2ToId3v1() { if (tagsParsingStatus() == ParsingStatus::NotParsedYet || !areTagsSupported() || !hasId3v2Tag()) { return false; } return createAppropriateTags(TagCreationSettings{ {}, TagCreationFlags::MergeMultipleSuccessiveId3v2Tags | TagCreationFlags::KeepExistingId3v2Version, TagUsage::Always, TagUsage::Never, 3 }); } /*! * \brief Creates a Vorbis comment for the current file. * * This method does nothing if the tags/tracks of the current file haven't been parsed using * the parseTags() and parseTracks() methods. * * If the file has already a Vorbis comment no new tag will be created. * * To apply the created tag and other changings call the applyChanges() method. * * \returns Returns the Vorbis comment or nullptr if creation is not possible. * * \sa applyChanges() */ VorbisComment *MediaFileInfo::createVorbisComment() { switch (m_containerFormat) { case ContainerFormat::Ogg: if (m_container) { return static_cast(m_container.get())->createTag(TagTarget()); } break; case ContainerFormat::Flac: if (m_singleTrack) { return static_cast(m_singleTrack.get())->createVorbisComment(); } break; default:; } return nullptr; } /*! * \brief Removes all assigned Vorbis comment from the current file. * * To apply the removal and other changings call the applyChanges() method. * * \returns Returns whether there was an Vorbis comment assigned which could be removed. * * \sa applyChanges() */ bool MediaFileInfo::removeVorbisComment() { switch (m_containerFormat) { case ContainerFormat::Ogg: if (m_container) { bool hadTags = static_cast(m_container.get())->tagCount(); static_cast(m_container.get())->removeAllTags(); return hadTags; } break; case ContainerFormat::Flac: if (m_singleTrack) { return static_cast(m_singleTrack.get())->removeVorbisComment(); } break; default:; } return false; } /*! * \brief Stores all tags assigned to the current file in the specified vector. * \remarks * - Previous elements of the vector will not be cleared. * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if * those tags have not been stored to disk yet via applyChanges(). * - The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo is invalidated. */ void MediaFileInfo::tags(std::vector &tags) const { if (hasId3v1Tag()) { tags.push_back(m_id3v1Tag.get()); } for (const unique_ptr &tag : m_id3v2Tags) { tags.push_back(tag.get()); } if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) { if (auto *const vorbisComment = static_cast(m_singleTrack.get())->vorbisComment()) { tags.push_back(vorbisComment); } } if (m_container) { for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) { tags.push_back(m_container->tag(i)); } } } /*! * \brief Returns all tags assigned to the current file. * \remarks * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if * those tags have not been stored to disk yet via applyChanges(). * - The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo is invalidated. */ vector MediaFileInfo::tags() const { auto res = vector(); tags(res); return res; } /*! * \brief Returns an indication whether a tag of any format is assigned. * \remarks * - Includes tags which have only been assigned, e.g. via createAppropriateTags(), even if * those tags have not been stored to disk yet via applyChanges(). */ bool MediaFileInfo::hasAnyTag() const { return hasId3v1Tag() || hasId3v2Tag() || (m_container && m_container->tagCount()) || (m_containerFormat == ContainerFormat::Flac && static_cast(m_singleTrack.get())->vorbisComment()); } /*! * \brief Returns all tags parsed from the current file. * \remarks * - Previous elements of the vector will not be cleared. * - Does **not** include tags which have been assigned, e.g. via createAppropriateTags() but * have not been stored to disk yet via applyChanges(). * - The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo is invalidated. */ void MediaFileInfo::parsedTags(std::vector &tags) const { if (hasId3v1Tag() && m_id3v1Tag->size()) { tags.push_back(m_id3v1Tag.get()); } for (const unique_ptr &tag : m_id3v2Tags) { if (tag->size()) { tags.push_back(tag.get()); } } if (m_containerFormat == ContainerFormat::Flac && m_singleTrack) { if (auto *const vorbisComment = static_cast(m_singleTrack.get())->vorbisComment()) { if (vorbisComment->size()) { tags.push_back(vorbisComment); } } } if (m_container) { for (size_t i = 0, count = m_container->tagCount(); i < count; ++i) { if (auto *const tag = m_container->tag(i); tag->size()) { tags.push_back(tag); } } } } /*! * \brief Returns all tags parsed from the current file. * \remarks * - Does **not** include tags which have been assigned, e.g. via createAppropriateTags() but * have not been stored to disk yet via applyChanges(). * - The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo is invalidated. */ std::vector MediaFileInfo::parsedTags() const { auto res = vector(); parsedTags(res); return res; } /*! * \brief Reimplemented from BasicFileInfo::invalidated(). */ void MediaFileInfo::invalidated() { BasicFileInfo::invalidated(); clearParsingResults(); } /*! * \brief Internally used to save chanings of MP3/FLAC files and any other files which might have ID3 tags. */ void MediaFileInfo::makeMp3File(Diagnostics &diag, AbortableProgressFeedback &progress) { static const string context("making MP3/FLAC file"); // don't rewrite the complete file if there are no ID3v2/FLAC tags present or to be written if (!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty() && m_containerFormat != ContainerFormat::Flac) { // alter ID3v1 tag if (!m_id3v1Tag) { // remove ID3v1 tag if (!(m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag)) { diag.emplace_back(DiagLevel::Information, "Nothing to be changed.", context); return; } progress.updateStep("Removing ID3v1 tag ..."); stream().close(); 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: " + ec.message(), context); throw std::ios_base::failure("Unable to truncate file to remove ID3v1 tag."); } return; } else { // add or update ID3v1 tag if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) { progress.updateStep("Updating existing ID3v1 tag ..."); // ensure the file is still open / not readonly open(); stream().seekp(-128, ios_base::end); try { m_id3v1Tag->make(stream(), diag); } catch (const Failure &) { diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context); } } else { progress.updateStep("Adding new ID3v1 tag ..."); // ensure the file is still open / not readonly open(); stream().seekp(0, ios_base::end); try { m_id3v1Tag->make(stream(), diag); } catch (const Failure &) { diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context); } } // prevent deferring final write operations (to catch and handle possible errors here) stream().flush(); } return; } // ID3v2 needs to be modified FlacStream *const flacStream = (m_containerFormat == ContainerFormat::Flac ? static_cast(m_singleTrack.get()) : nullptr); progress.updateStep(flacStream ? "Updating FLAC tags ..." : "Updating ID3v2 tags ..."); // prepare ID3v2 tags auto makers = std::vector(); makers.reserve(m_id3v2Tags.size()); auto tagsSize = std::uint64_t(); for (auto &tag : m_id3v2Tags) { try { makers.emplace_back(tag->prepareMaking(diag)); tagsSize += makers.back().requiredSize(); } catch (const Failure &) { } } // determine stream offset and make track/format specific metadata 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); tagsSize += static_cast(flacMetaData.tellp()); streamOffset = flacStream->streamOffset(); } else { // make no further metadata, just use the container offset as stream offset streamOffset = static_cast(m_containerOffset); } // check whether rewrite is required 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 padding = streamOffset - tagsSize; // -> check whether the new padding matches specifications if (padding < minPadding() || padding > maxPadding()) { rewriteRequired = true; } } if (makers.empty() && !flacStream) { // an ID3v2 tag is not written and it is not a FLAC stream // -> can't include padding if (padding) { // but padding would be present -> need to rewrite padding = 0; // can't write the preferred padding despite rewriting rewriteRequired = true; } } else if (rewriteRequired) { // rewriting is forced or new ID3v2 tag is too big for available space // -> use preferred padding when rewriting anyways padding = preferredPadding(); } else if (makers.empty() && flacStream && padding && padding < 4) { // no ID3v2 tag -> must include padding in FLAC stream // but padding of 1, 2, and 3 byte isn't possible -> need to rewrite padding = preferredPadding(); rewriteRequired = true; } if (rewriteRequired && flacStream && makers.empty() && padding) { // the first 4 byte of FLAC padding actually don't count because these // can not be used for additional meta data padding += 4; } progress.updateStep(rewriteRequired ? "Preparing streams for rewriting ..." : "Preparing streams for updating ..."); // setup stream(s) for writing // -> define variables needed to handle output stream and backup stream (required when rewriting the file) string originalPath = path(), backupPath; NativeFileStream &outputStream = stream(); NativeFileStream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required if (rewriteRequired) { if (m_saveFilePath.empty()) { // move current file to temp dir and reopen it as backupStream, recreate original file try { BackupHelper::createBackupFileCanonical(backupDirectory(), originalPath, backupPath, outputStream, backupStream); // recreate original file, define buffer variables outputStream.open(originalPath, ios_base::out | ios_base::binary | ios_base::trunc); } catch (const std::ios_base::failure &failure) { diag.emplace_back( DiagLevel::Critical, argsToString("Creation of temporary file (to rewrite the original file) failed: ", failure.what()), context); throw; } } else { // open the current file as backupStream and create a new outputStream at the specified "save file path" try { close(); backupStream.exceptions(ios_base::badbit | ios_base::failbit); backupStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::binary); outputStream.open(BasicFileInfo::pathForOpen(m_saveFilePath).data(), ios_base::out | ios_base::binary | ios_base::trunc); } catch (const std::ios_base::failure &failure) { diag.emplace_back(DiagLevel::Critical, argsToString("Opening streams to write output file failed: ", failure.what()), context); throw; } } } else { // !rewriteRequired // reopen original file to ensure it is opened for writing try { close(); outputStream.open(BasicFileInfo::pathForOpen(path()).data(), ios_base::in | ios_base::out | ios_base::binary); } catch (const std::ios_base::failure &failure) { diag.emplace_back(DiagLevel::Critical, argsToString("Opening the file with write permissions failed: ", failure.what()), context); throw; } } // TODO: fix code duplication // start actual writing try { // ensure we can cast padding safely to uint32 if (padding > numeric_limits::max()) { padding = numeric_limits::max(); diag.emplace_back( DiagLevel::Critical, argsToString("Preferred padding is not supported. Setting preferred padding to ", padding, '.'), context); } if (!makers.empty()) { // write ID3v2 tags progress.updateStep("Writing ID3v2 tag ..."); for (auto i = makers.begin(), end = makers.end() - 1; i != end; ++i) { i->make(outputStream, 0, diag); } // include padding into the last ID3v2 tag makers.back().make(outputStream, (flacStream && padding && padding < 4) ? 0 : static_cast(padding), diag); } if (flacStream) { if (padding && startOfLastMetaDataBlock) { // if appending padding, ensure the last flag of the last "METADATA_BLOCK_HEADER" is not set flacMetaData.seekg(startOfLastMetaDataBlock); flacMetaData.seekp(startOfLastMetaDataBlock); flacMetaData.put(static_cast(flacMetaData.peek()) & (0x80u - 1)); flacMetaData.seekg(0); } // write FLAC metadata outputStream << flacMetaData.rdbuf(); // write padding if (padding) { flacStream->makePadding(outputStream, static_cast(padding), true, diag); } } if (makers.empty() && !flacStream) { // just write padding (however, padding should be set to 0 in this case?) for (; padding; --padding) { outputStream.put(0); } } // copy / skip actual stream data // -> determine media data size std::uint64_t mediaDataSize = size() - streamOffset; if (m_fileStructureFlags & MediaFileStructureFlags::ActualExistingId3v1Tag) { mediaDataSize -= 128; } if (rewriteRequired) { // copy data from original file switch (m_containerFormat) { case ContainerFormat::MpegAudioFrames: progress.updateStep("Writing MPEG audio frames ..."); break; default: progress.updateStep("Writing data ..."); } backupStream.seekg(static_cast(streamOffset)); CopyHelper<0x4000> copyHelper; 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(mediaDataSize), ios_base::cur); } // write ID3v1 tag if (m_id3v1Tag) { progress.updateStep("Writing ID3v1 tag ..."); try { m_id3v1Tag->make(stream(), diag); } catch (const Failure &) { diag.emplace_back(DiagLevel::Warning, "Unable to write ID3v1 tag.", context); } } // handle streams if (rewriteRequired) { // report new size reportSizeChanged(static_cast(outputStream.tellp())); // "save as path" is now the regular path if (!saveFilePath().empty()) { reportPathChanged(saveFilePath()); m_saveFilePath.clear(); } // prevent deferring final write operations (to catch and handle possible errors here); stream is useless for further // usage anyways because it is write-only outputStream.close(); } else { const auto newSize = static_cast(outputStream.tellp()); if (newSize < size()) { // file is smaller after the modification -> truncate // -> prevent deferring final write operations outputStream.close(); // -> truncate file 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: " + ec.message(), context); } } else { // file is longer after the modification // -> prevent deferring final write operations (to catch and handle possible errors here) outputStream.flush(); // -> report new size reportSizeChanged(newSize); } } } catch (...) { BackupHelper::handleFailureAfterFileModifiedCanonical(*this, originalPath, backupPath, outputStream, backupStream, diag, context); } } } // namespace TagParser