#include "./mediafileinfo.h" #include "./exceptions.h" #include "./tag.h" #include "./signature.h" #include "./abstracttrack.h" #include "./backuphelper.h" #include "./id3/id3v1tag.h" #include "./id3/id3v2tag.h" #include "./wav/waveaudiostream.h" #include "./mpegaudio/mpegaudioframestream.h" #include "./adts/adtsstream.h" #include "./mp4/mp4container.h" #include "./mp4/mp4atom.h" #include "./mp4/mp4tag.h" #include "./mp4/mp4ids.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 #include #include #include #include #include #include #include #include using namespace std; using namespace IoUtilities; using namespace ConversionUtilities; using namespace ChronoUtilities; namespace Media { #ifdef FORCE_FULL_PARSE_DEFAULT # define MEDIAINFO_CPP_FORCE_FULL_PARSE true #else # define MEDIAINFO_CPP_FORCE_FULL_PARSE false #endif /*! * \class Media::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. */ MediaFileInfo::MediaFileInfo() : m_containerParsingStatus(ParsingStatus::NotParsedYet), m_containerFormat(ContainerFormat::Unknown), m_containerOffset(0), m_actualExistingId3v1Tag(false), m_tracksParsingStatus(ParsingStatus::NotParsedYet), m_tagsParsingStatus(ParsingStatus::NotParsedYet), m_chaptersParsingStatus(ParsingStatus::NotParsedYet), m_attachmentsParsingStatus(ParsingStatus::NotParsedYet), m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE), m_forceRewrite(true), m_minPadding(0), m_maxPadding(0), m_preferredPadding(0), m_tagPosition(ElementPosition::BeforeData), m_forceTagPosition(true), m_indexPosition(ElementPosition::BeforeData), m_forceIndexPosition(true) {} /*! * \brief Constructs a new MediaFileInfo for the specified file. * * \param path Specifies the absolute or relative path of the file. */ MediaFileInfo::MediaFileInfo(const string &path) : BasicFileInfo(path), m_containerParsingStatus(ParsingStatus::NotParsedYet), m_containerFormat(ContainerFormat::Unknown), m_containerOffset(0), m_actualExistingId3v1Tag(false), m_tracksParsingStatus(ParsingStatus::NotParsedYet), m_tagsParsingStatus(ParsingStatus::NotParsedYet), m_chaptersParsingStatus(ParsingStatus::NotParsedYet), m_attachmentsParsingStatus(ParsingStatus::NotParsedYet), m_forceFullParse(MEDIAINFO_CPP_FORCE_FULL_PARSE), m_forceRewrite(true), m_minPadding(0), m_maxPadding(0), m_preferredPadding(0), m_tagPosition(ElementPosition::BeforeData), m_forceTagPosition(true), m_indexPosition(ElementPosition::BeforeData), m_forceIndexPosition(true) {} /*! * \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. * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. * * \sa isContainerParsed() * \sa parseTracks() * \sa parseTag() * \sa parseChapters() * \sa parseEverything() */ void MediaFileInfo::parseContainerFormat() { if(containerParsingStatus() != ParsingStatus::NotParsedYet) { // there's no need to read the container format twice return; } invalidateStatus(); 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; // read signatrue char buff[16]; const char *const buffEnd = buff + sizeof(buff), *buffOffset; startParsingSignature: if(size() - m_containerOffset >= 16) { stream().seekg(m_containerOffset, ios_base::beg); stream().read(buff, sizeof(buff)); // skip zero bytes/padding size_t bytesSkipped = 0; for(buffOffset = buff; buffOffset != buffEnd && !(*buffOffset); ++buffOffset, ++bytesSkipped); if(bytesSkipped >= 4) { m_containerOffset += bytesSkipped; // give up after 0x100 bytes if((m_paddingSize += bytesSkipped) >= 0x100u) { m_containerParsingStatus = ParsingStatus::NotSupported; m_containerFormat = ContainerFormat::Unknown; return; } goto startParsingSignature; } if(m_paddingSize) { addNotification(NotificationType::Warning, ConversionUtilities::numberToString(m_paddingSize) + " zero-bytes skipped at the beginning of the file.", context); } // parse signature switch(m_containerFormat = parseSignature(buff, sizeof(buff))) { case ContainerFormat::Id2v2Tag: m_actualId3v2TagOffsets.push_back(m_containerOffset); if(m_actualId3v2TagOffsets.size() == 2) { addNotification(NotificationType::Warning, "There is more then just one ID3v2 header at the beginning of the file.", context); } stream().seekg(m_containerOffset + 6, ios_base::beg); stream().read(buff, 4); // set the container offset to skip ID3v2 header m_containerOffset += ConversionUtilities::toNormalInt(ConversionUtilities::BE::toUInt32(buff)) + 10; goto startParsingSignature; // read signature again case ContainerFormat::Mp4: { m_container = make_unique(*this, m_containerOffset); NotificationList notifications; try { static_cast(m_container.get())->validateElementStructure(notifications, &m_paddingSize); } catch (Failure &) { m_containerParsingStatus = ParsingStatus::CriticalFailure; } addNotifications(notifications); break; } case ContainerFormat::Ebml: { auto container = make_unique(*this, m_containerOffset); NotificationList notifications; try { container->parseHeader(); if(container->documentType() == "matroska") { m_containerFormat = ContainerFormat::Matroska; } else if(container->documentType() == "webm") { m_containerFormat = ContainerFormat::Webm; } if(m_forceFullParse) { // validating the element structure of Matroska files takes too long when // parsing big files so do this only when explicitely desired container->validateElementStructure(notifications, &m_paddingSize); container->validateIndex(); } } catch(Failure &) { m_containerParsingStatus = ParsingStatus::CriticalFailure; } m_container = move(container); addNotifications(notifications); break; } case ContainerFormat::Ogg: m_container = make_unique(*this, m_containerOffset); static_cast(m_container.get())->setChecksumValidationEnabled(m_forceFullParse); break; default: ; } } 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. * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. * * \remarks parseContainerFormat() is called before the tracks will be parsed. * * \sa areTracksParsed() * \sa parseContainerFormat() * \sa parseTag() * \sa parseChapters() * \sa parseEverything() */ void MediaFileInfo::parseTracks() { if(tracksParsingStatus() != ParsingStatus::NotParsedYet) { // there's no need to read the tracks twice return; } parseContainerFormat(); // ensure the container format has been load yet static const string context("parsing tracks"); try { if(m_container) { m_container->parseTracks(); } else { switch(m_containerFormat) { case ContainerFormat::RiffWave: m_singleTrack = make_unique(stream(), m_containerOffset); break; case ContainerFormat::MpegAudioFrames: m_singleTrack = make_unique(stream(), m_containerOffset); break; case ContainerFormat::Adts: m_singleTrack = make_unique(stream(), m_containerOffset); break; default: throw NotImplementedException(); } m_singleTrack->parseHeader(); } m_tracksParsingStatus = ParsingStatus::Ok; } catch (NotImplementedException &) { addNotification(NotificationType::Information, "Parsing tracks is not implemented for the container format of the file.", context); m_tracksParsingStatus = ParsingStatus::NotSupported; } catch (Failure &) { addNotification(NotificationType::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. * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. * * \remarks parseContainerFormat() is called before the tags informations will be parsed. * * \sa isTagParsed() * \sa parseContainerFormat() * \sa parseTracks() * \sa parseChapters() * \sa parseEverything() */ void MediaFileInfo::parseTags() { if(tagsParsingStatus() != ParsingStatus::NotParsedYet) { // there's no need to read the tags twice return; } parseContainerFormat(); // ensure the container format has been load yet static const string context("parsing tag"); // check for id3v1 tag if(size() >= 128) { m_id3v1Tag = make_unique(); try { m_id3v1Tag->parse(stream(), true); m_actualExistingId3v1Tag = true; } catch(NoDataFoundException &) { m_id3v1Tag.reset(); // no ID3v1 tag found } catch(Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; addNotification(NotificationType::Critical, "Unable to parse ID3v1 tag.", context); } } // 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()); m_paddingSize += id3v2Tag->paddingSize(); } catch(NoDataFoundException &) { continue; } catch(Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; addNotification(NotificationType::Critical, "Unable to parse ID3v2 tag.", context); } m_id3v2Tags.emplace_back(id3v2Tag.release()); } if(m_container) { try { m_container->parseTags(); } catch (NotImplementedException &) { if(m_tagsParsingStatus == ParsingStatus::NotParsedYet) { // do not override parsing status from ID3 tags here m_tagsParsingStatus = ParsingStatus::NotSupported; } addNotification(NotificationType::Information, "Parsing tags is not implemented for the container format of the file.", context); } catch (Failure &) { m_tagsParsingStatus = ParsingStatus::CriticalFailure; addNotification(NotificationType::Critical, "Unable to parse tag.", context); } } if(m_tagsParsingStatus == ParsingStatus::NotParsedYet) { // do not override error status here m_tagsParsingStatus = ParsingStatus::Ok; } } /*! * \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. * \throws Throws Media::Failure or a derived exception when a parsing * error occurs. * * \remarks parseContainerFormat() is called before the tags informations will be parsed. * * \sa areChaptersParsed() * \sa parseContainerFormat() * \sa parseTracks() * \sa parseTags() * \sa parseEverything() */ void MediaFileInfo::parseChapters() { if(chaptersParsingStatus() != ParsingStatus::NotParsedYet) { // there's no need to read the chapters twice return; } static const string context("parsing chapters"); try { if(m_container) { m_container->parseChapters(); m_chaptersParsingStatus = ParsingStatus::Ok; } else { throw NotImplementedException(); } } catch (NotImplementedException &) { m_chaptersParsingStatus = ParsingStatus::NotSupported; addNotification(NotificationType::Information, "Parsing chapters is not implemented for the container format of the file.", context); } catch (Failure &) { m_chaptersParsingStatus = ParsingStatus::CriticalFailure; addNotification(NotificationType::Critical, "Unable to parse chapters.", context); } } void MediaFileInfo::parseAttachments() { if(attachmentsParsingStatus() != ParsingStatus::NotParsedYet) { // there's no need to read the attachments twice return; } static const string context("parsing attachments"); try { if(m_container) { m_container->parseAttachments(); m_attachmentsParsingStatus = ParsingStatus::Ok; } else { throw NotImplementedException(); } } catch (NotImplementedException &) { m_attachmentsParsingStatus = ParsingStatus::NotSupported; addNotification(NotificationType::Information, "Parsing attachments is not implemented for the container format of the file.", context); } catch (Failure &) { m_attachmentsParsingStatus = ParsingStatus::CriticalFailure; addNotification(NotificationType::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(); * \sa parseTracks(); * \sa parseTag(); */ void MediaFileInfo::parseEverything() { parseContainerFormat(); parseTracks(); parseTags(); parseChapters(); parseAttachments(); } /*! * \brief Ensures appropriate tags are created. * \param treatUnknownFilesAsMp3Files Specifies whether unknown file formats should be treated as MP3. * \param id3v1usage Specifies the usage of ID3v1 when creating tags for MP3 files. * \param id3v2usage Specifies the usage of ID3v2 when creating tags for MP3 files. * \param mergeMultipleSuccessiveId3v2Tags Specifies whether multiple successive ID3v2 tags should be merged (see mergeId3v2Tags()). * \param keepExistingId3v2version Specifies whether the version of existing ID3v2 tags should be updated. * \param id3v2version Specifies the IDv2 version to be used. Valid values are 2, 3 and 4. * \param requiredTargets Specifies the required targets. Targets are ignored if not supported by the container. * \return Returns an indication whether appropriate tags could be created for the file. * \remarks * - 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. These arguments are ignored when creating * tags for other known file formats such as MP4. * - 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 the file already has appropriate tags. * - This is only a convenience method. The task can 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 (\a id3v2usage is set to TagUsage::Never) * and an ID3v1 tag will be created instead not all fields can be transfered. */ bool MediaFileInfo::createAppropriateTags(bool treatUnknownFilesAsMp3Files, TagUsage id3v1usage, TagUsage id3v2usage, bool mergeMultipleSuccessiveId3v2Tags, bool keepExistingId3v2version, uint32 id3v2version, const std::vector &requiredTargets) { // check if tags need to be created/adjusted/removed bool targetsRequired = !requiredTargets.empty() && (requiredTargets.size() != 1 && requiredTargets.front().isEmpty()); bool targetsSupported = false; if(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 *tag = m_container->createTag(); if(tag) { if((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(); } } else { // no container object present; creation of ID3 tag is possible if(!hasAnyTag() && !treatUnknownFilesAsMp3Files) { switch(containerFormat()) { case ContainerFormat::MpegAudioFrames: case ContainerFormat::Adts: break; default: return false; } } // create ID3 tags according to id3v2usage and id3v2usage if(id3v1usage == TagUsage::Always) { // always create ID3v1 tag -> ensure there is one createId3v1Tag(); } if(id3v2usage == TagUsage::Always) { // always create ID3v2 tag -> ensure there is one and set version if(!hasId3v2Tag()) { createId3v2Tag()->setVersion(id3v2version, 0); } } if(mergeMultipleSuccessiveId3v2Tags) { mergeId3v2Tags(); } // remove ID3 tags according to id3v2usage and id3v2usage if(id3v1usage == TagUsage::Never) { if(hasId3v1Tag()) { if(hasId3v2Tag()) { // transfer tags to ID3v2 tag before removing id3v2Tags().front()->insertValues(*id3v1Tag(), false); } removeId3v1Tag(); } } if(id3v2usage == TagUsage::Never) { if(hasId3v1Tag()) { // transfer tags to ID3v1 tag before removing for(const auto &tag : id3v2Tags()) { id3v1Tag()->insertValues(*tag, false); } } removeAllId3v2Tags(); } else if(!keepExistingId3v2version) { // set version of ID3v2 tag according user preferences for(const auto &tag : id3v2Tags()) { tag->setVersion(id3v2version, 0); } } } if(targetsRequired && !targetsSupported) { addNotification(NotificationType::Warning, "The container/tags do not support targets. The specified targets are ignored.", "creating tags"); } 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 Media::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() { const string context("making file"); addNotification(NotificationType::Information, "Changes are about to be applied.", context); bool previousParsingSuccessful = true; switch(tagsParsingStatus()) { case ParsingStatus::Ok: case ParsingStatus::NotSupported: break; default: previousParsingSuccessful = false; addNotification(NotificationType::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; addNotification(NotificationType::Critical, "Tracks have to be parsed without critical errors before changes can be applied.", context); } // switch(chaptersParsingStatus()) { // case ParsingStatus::Ok: // case ParsingStatus::NotSupported: // break; // default: // previousParsingSuccessful = false; // addNotification(NotificationType::Critical, "Chapters have to be parsed without critical errors before changes can be applied.", context); // } // switch(attachmentsParsingStatus()) { // case ParsingStatus::Ok: // case ParsingStatus::NotSupported: // break; // default: // previousParsingSuccessful = false; // addNotification(NotificationType::Critical, "Attachments 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()) { addNotification(NotificationType::Warning, "Assigned ID3v1 tag can't be attached and will be ignored.", context); } if(hasId3v2Tag()) { addNotification(NotificationType::Warning, "Assigned ID3v2 tag can't be attached and will be ignored.", context); } m_container->forwardStatusUpdateCalls(this); m_tracksParsingStatus = ParsingStatus::NotParsedYet; m_tagsParsingStatus = ParsingStatus::NotParsedYet; try { m_container->makeFile(); } catch(...) { clearParsingResults(); throw; } } else { // implementation if no container object is present // assume the file is a MP3 file try { makeMp3File(); } catch(...) { 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() */ const char *MediaFileInfo::containerFormatAbbreviation() const { MediaType mediaType = MediaType::Unknown; unsigned int version = 0; switch(m_containerFormat) { case ContainerFormat::Ogg: 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 Media::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() */ const char *MediaFileInfo::mimeType() const { switch(m_containerFormat) { case ContainerFormat::Asf: return "video/x-ms-asf"; case ContainerFormat::Gif87a: case ContainerFormat::Gif89a: return "image/gif"; case ContainerFormat::Jpeg: return "image/jpeg"; case ContainerFormat::Png: return "image/png"; case ContainerFormat::MpegAudioFrames: return "audio/mpeg"; case ContainerFormat::Mp4: if(hasTracksOfType(MediaType::Video)) { return "video/mp4"; } return "audio/mp4"; case ContainerFormat::Ogg: if(hasTracksOfType(MediaType::Video)) { return "video/ogg"; } return "audio/ogg"; case ContainerFormat::Matroska: if(hasTracksOfType(MediaType::Video)) { return "video/x-matroska"; } return "audio/x-matroska"; case ContainerFormat::Bzip2: return "application/x-bzip"; case ContainerFormat::Gzip: return "application/gzip"; case ContainerFormat::Lha: return "application/x-lzh-compressed"; case ContainerFormat::Rar: return "application/x-rar-compressed"; case ContainerFormat::Lzip: return "application/x-lzip"; case ContainerFormat::Zip: return "application/zip"; case ContainerFormat::SevenZ: return "application/x-7z-compressed"; case ContainerFormat::WindowsBitmap: return "image/bmp"; case ContainerFormat::WindowsIcon: return "image/vnd.microsoft.icon"; default: return ""; } } /*! * \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 gets invalidated. * * \sa parseTracks() */ vector MediaFileInfo::tracks() const { vector res; if(m_singleTrack) { res.push_back(m_singleTrack.get()); } if(m_container) { for(size_t i = 0, count = m_container->trackCount(); i < count; ++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) { 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 is known; otherwise * returns a TimeSpan with zero ticks. * * parseTracks() needs to be called before. Otherwise this * method always returns false. * * \sa parseTracks() */ ChronoUtilities::TimeSpan MediaFileInfo::duration() const { TimeSpan res; if(m_container) { res = m_container->duration(); } else { for(const AbstractTrack *track : tracks()) { if(track->duration() > res) { res = track->duration(); } } } return res; } /*! * \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. * * \sa applyChanges() */ bool MediaFileInfo::removeId3v1Tag() { if(tagsParsingStatus() != ParsingStatus::NotParsedYet) { 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 The \a tag will be destroyed by the MediaFileInfo if it could be removed. * * \sa applyChanges() */ bool MediaFileInfo::removeId3v2Tag(Id3v2Tag *tag) { if(tagsParsingStatus() != ParsingStatus::NotParsedYet) { 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. * * \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 gets invalidated. * * \sa applyChanges() */ Id3v2Tag *MediaFileInfo::createId3v2Tag() { if(!m_id3v2Tags.size()) { m_id3v2Tags.emplace_back(make_unique()); } return m_id3v2Tags.front().get(); } /*! * \brief Removes a possibly assigned \a tag from the current file. * * \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). * * To apply the removal and other changings call the applyChanges() method. * * \sa applyChanges() */ void MediaFileInfo::removeTag(Tag *tag) { if(tag) { if(m_container) { m_container->removeTag(tag); } if(m_id3v1Tag.get() == tag) { m_id3v1Tag.reset(); } for(auto i = m_id3v2Tags.begin(), end = m_id3v2Tags.end(); i != end; ++i) { if(i->get() == tag) { m_id3v2Tags.erase(i); break; } } } } /*! * \brief Removes all assigned tags from the file. * * To apply the removal and other changings call the applyChanges() method. */ void MediaFileInfo::removeAllTags() { if(m_container) { m_container->removeAllTags(); } 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 { if(hasAnyTag()) { return true; } switch(m_containerFormat) { case ContainerFormat::Mp4: case ContainerFormat::MpegAudioFrames: case ContainerFormat::Ogg: case ContainerFormat::Matroska: case ContainerFormat::Webm: case ContainerFormat::Adts: return true; default: return false; } } /*! * \brief Returns a pointer to the assigned MP4 tag or nullptr if none is assigned. * * \remarks The MediaFileInfo keeps the ownership over the returned * pointer. The returned MP4 tag will be destroyed when the * MediaFileInfo gets invalidated. */ Mp4Tag *MediaFileInfo::mp4Tag() const { // simply return the first tag here since MP4 files never contain multiple tags return m_containerFormat == ContainerFormat::Mp4 && 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 gets 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 all chapters assigned to the current file. * * \remarks The MediaFileInfo keeps the ownership over the chapters which will be * destroyed when the MediaFileInfo gets invalidated. */ vector MediaFileInfo::chapters() const { vector res; if(m_container) { 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 attachments which will be * destroyed when the MediaFileInfo gets invalidated. */ vector MediaFileInfo::attachments() const { vector res; if(m_container) { 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 Returns an indication whether at least one related object (track, * tag, container) has notifications. */ bool MediaFileInfo::haveRelatedObjectsNotifications() const { if(m_container) { if(m_container->hasNotifications()) { return true; } } for(const auto *track : tracks()) { if(track->hasNotifications()) { return true; } } for(const auto *tag : tags()) { if(tag->hasNotifications()) { return true; } } for(const auto *chapter : chapters()) { if(chapter->hasNotifications()) { return true; } } return false; } /*! * \brief Returns the worst notification type including related objects such as track, * tag and container. */ NotificationType MediaFileInfo::worstNotificationTypeIncludingRelatedObjects() const { NotificationType type = worstNotificationType(); if(type == Notification::worstNotificationType()) { return type; } if(m_container) { type |= m_container->worstNotificationType(); } if(type == Notification::worstNotificationType()) { return type; } for(const auto *track : tracks()) { type |= track->worstNotificationType(); if(type == Notification::worstNotificationType()) { return type; } } for(const auto *tag : tags()) { type |= tag->worstNotificationType(); if(type == Notification::worstNotificationType()) { return type; } } for(const auto *chapter : chapters()) { type |= chapter->worstNotificationType(); if(type == Notification::worstNotificationType()) { return type; } } return type; } /*! * \brief Returns the notifications of the current instance and all related * objects (tracks, tags, container, ...). * * \remarks The specified list is not cleared before notifications are added. */ void MediaFileInfo::gatherRelatedNotifications(NotificationList ¬ifications) const { notifications.insert(notifications.end(), this->notifications().cbegin(), this->notifications().cend()); if(m_container) { notifications.insert(notifications.end(), m_container->notifications().cbegin(), m_container->notifications().cend()); } for(const auto *track : tracks()) { notifications.insert(notifications.end(), track->notifications().cbegin(), track->notifications().cend()); } for(const auto *tag : tags()) { notifications.insert(notifications.end(), tag->notifications().cbegin(), tag->notifications().cend()); } for(const auto *chapter : chapters()) { notifications.insert(notifications.end(), chapter->notifications().cbegin(), chapter->notifications().cend()); } for(const auto *attachment : attachments()) { notifications.insert(notifications.end(), attachment->notifications().cbegin(), attachment->notifications().cend()); } } /*! * \brief Returns the notifications of the current instance and all related * objects (tracks, tags, container, ...). */ NotificationList MediaFileInfo::gatherRelatedNotifications() const { NotificationList notifications; gatherRelatedNotifications(notifications); return notifications; } /*! * \brief Clears all parsing results and assigned/created/changed information such as * container format, tracks, tags, ... * * This allows a rescan of the file using parsing methods like parseContainerFormat(). * (These methods do nothing if the information to be parsed has already been gathered.) * * \remarks Any pointers previously returned by tags(), tracks(), ... object are 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_actualExistingId3v1Tag = false; m_container.reset(); m_singleTrack.reset(); } /*! * \brief Merges the assigned ID3v2 tags. * * 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 seperate. * 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) { Id3v2Tag &first = **begin; auto isecond = begin + 1; if(isecond != end) { for(auto i = isecond; i != end; ++i) { first.insertFields(**i, false); } m_id3v2Tags.erase(isecond, end - 1); } } } /*! * \brief Stores all tags assigned to the current file in the specified vector. * * Previous elements of the vector will not be cleared. * * \remarks The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo gets invalidated. */ void MediaFileInfo::tags(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_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 The MediaFileInfo keeps the ownership over the tags which will be * destroyed when the MediaFileInfo gets invalidated. */ vector MediaFileInfo::tags() const { vector res; tags(res); return res; } /*! * \brief Reimplemented from BasicFileInfo::invalidated(). */ void MediaFileInfo::invalidated() { BasicFileInfo::invalidated(); invalidateStatus(); invalidateNotifications(); clearParsingResults(); } /*! * \brief Internally used to chanings of a MP3 file (or theoretically any file) with ID3 tags. */ void MediaFileInfo::makeMp3File() { const string context("making MP3 file"); // there's no need to rewrite the complete file if(m_id3v2Tags.size() == 0 && m_actualId3v2TagOffsets.size() == 0) { if(m_actualExistingId3v1Tag) { // there is currently an ID3v1 tag at the end of the file if(m_id3v1Tag) { // the file shall still have an ID3v1 tag updateStatus("No need to rewrite the whole file, just writing ID3v1 tag ..."); open(); // ensure the file is still open stream().seekp(-128, ios_base::end); try { m_id3v1Tag->make(stream()); } catch(Failure &) { addNotification(NotificationType::Warning, "Unable to write ID3v1 tag.", context); } } else { // the currently existing id3v1 tag shall be removed updateStatus("No need to rewrite the whole file, just truncating it to remove ID3v1 tag ..."); stream().close(); if(truncate(path().c_str(), size() - 128) == 0) { reportSizeChanged(size() - 128); } else { addNotification(NotificationType::Critical, "Unable to truncate file to remove ID3v1 tag.", context); throw ios_base::failure("Unable to truncate file to remove ID3v1 tag."); } } } else { // the doesn't file need to be rewritten if(m_id3v1Tag) { updateStatus("No need to rewrite the whole file, just writing ID3v1 tag."); open(); // ensure the file is still open stream().seekp(0, ios_base::end); try { m_id3v1Tag->make(stream()); } catch(Failure &) { addNotification(NotificationType::Warning, "Unable to write ID3v1 tag.", context); } } else { addNotification(NotificationType::Information, "Nothing to be changed.", context); } } } else { // the file needs to be rewritten updateStatus("Prepareing for rewriting MP3 file ..."); close(); // close the file (if its opened) string backupPath; fstream backupStream; try { BackupHelper::createBackupFile(path(), backupPath, backupStream); backupStream.seekg(m_containerOffset); // recreate original file with new/changed ID3 tags stream().open(path(), ios_base::out | ios_base::binary | ios_base::trunc); updateStatus("Writing ID3v2 tag ..."); // write id3v2 tags int counter = 1; for(auto &id3v2Tag : m_id3v2Tags) { try { id3v2Tag->make(stream()); } catch(Failure &) { if(m_id3v2Tags.size()) { addNotification(NotificationType::Warning, "Unable to write " + ConversionUtilities::numberToString(counter) + ". ID3v2 tag.", context); } else { addNotification(NotificationType::Warning, "Unable to write ID3v2 tag.", context); } } ++counter; } // recopy backup updateStatus("Writing mpeg audio frames ..."); uint64 remainingBytes = size() - backupStream.tellg(), read; if(m_actualExistingId3v1Tag) { remainingBytes -= 128; } const unsigned int bufferSize = 0x4000; char buffer[bufferSize]; while(remainingBytes > 0) { if(isAborted()) { throw OperationAbortedException(); } backupStream.read(buffer, remainingBytes > bufferSize ? bufferSize : remainingBytes); read = backupStream.gcount(); stream().write(buffer, read); remainingBytes -= read; updatePercentage(static_cast(backupStream.tellg()) / static_cast(size())); } // write id3v1 tag updateStatus("Writing ID3v1 tag ..."); if(m_id3v1Tag) { try { m_id3v1Tag->make(stream()); } catch(Failure &) { addNotification(NotificationType::Warning, "Unable to write ID3v1 tag.", context); } } stream().flush(); // ensure everything has been actually written reportSizeChanged(stream().tellp()); // report new size close(); // stream is useless for further usage because it is write only } catch(OperationAbortedException &) { addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context); BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream); throw; } catch(ios_base::failure &ex) { addNotification(NotificationType::Critical, "IO error occured when rewriting file to apply new tag information.\n" + string(ex.what()), context); BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, stream(), backupStream); throw; } } } }