#include "./scriptapi.h" #include "./fieldmapping.h" #include "./helper.h" #include "../application/knownfieldmodel.h" #include "../dbquery/dbquery.h" #include "../misc/utility.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QT_BEGIN_NAMESPACE uint qHash(const TagParser::KnownField key, uint seed) noexcept { return ::qHash(static_cast>(key), seed); } QT_END_NAMESPACE #endif namespace Cli { constexpr auto nativeUtf16Encoding = TagParser::TagTextEncoding:: #if defined(CONVERSION_UTILITIES_BYTE_ORDER_LITTLE_ENDIAN) Utf16LittleEndian #else Utf16BigEndian #endif ; const std::string UtilityObject::s_defaultContext = std::string("executing JavaScript"); UtilityObject::UtilityObject(QJSEngine *engine) : QObject(engine) , m_engine(engine) , m_context(nullptr) , m_diag(nullptr) { } void UtilityObject::log(const QString &message) { std::cout << message.toStdString() << std::endl; } void UtilityObject::diag(const QString &level, const QString &message, const QString &context) { if (!m_diag) { return; } static const auto mapping = QHash({ { QStringLiteral("fatal"), TagParser::DiagLevel::Fatal }, { QStringLiteral("critical"), TagParser::DiagLevel::Critical }, { QStringLiteral("error"), TagParser::DiagLevel::Critical }, { QStringLiteral("warning"), TagParser::DiagLevel::Warning }, { QStringLiteral("info"), TagParser::DiagLevel::Information }, { QStringLiteral("information"), TagParser::DiagLevel::Information }, { QStringLiteral("debug"), TagParser::DiagLevel::Debug }, }); m_diag->emplace_back(mapping.value(level.toLower(), TagParser::DiagLevel::Debug), message.toStdString(), context.isEmpty() ? (m_context ? *m_context : s_defaultContext) : context.toStdString()); } int UtilityObject::exec() { return QCoreApplication::exec(); } void UtilityObject::exit(int retcode) { QCoreApplication::exit(retcode); } QJSValue UtilityObject::readEnvironmentVariable(const QString &variable, const QJSValue &defaultValue) const { const auto variableUtf8 = variable.toUtf8(); if (qEnvironmentVariableIsSet(variableUtf8.data())) { return QJSValue(qEnvironmentVariable(variableUtf8.data())); } else { return QJSValue(defaultValue); } } QJSValue UtilityObject::readDirectory(const QString &path) { auto dir = QDir(path); return dir.exists() ? m_engine->toScriptValue(dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) : QJSValue(); } QJSValue UtilityObject::readFile(const QString &path) { if (auto file = QFile(path); file.open(QFile::ReadOnly)) { if (auto data = file.readAll(); file.error() != QFile::NoError) { return m_engine->toScriptValue(std::move(data)); } } return QJSValue(); } QJSValue UtilityObject::openFile(const QString &path) { if (!m_diag) { return QJSValue(); } auto mediaFileInfo = std::make_unique(path.toStdString()); auto feedback = TagParser::AbortableProgressFeedback(); try { mediaFileInfo->open(true); mediaFileInfo->parseEverything(*m_diag, feedback); } catch (const std::exception &e) { m_diag->emplace_back(TagParser::DiagLevel::Critical, CppUtilities::argsToString("Unable to open \"", mediaFileInfo->path(), "\": ", e.what()), m_context ? *m_context : s_defaultContext); return QJSValue(); } auto mediaFileInfoObj = new MediaFileInfoObject(std::move(mediaFileInfo), *m_diag, m_engine, false, m_engine); return m_engine->newQObject(mediaFileInfoObj); } QJSValue UtilityObject::runProcess(const QString &path, const QJSValue &args, int timeout) { auto res = m_engine->newObject(); #ifdef CPP_UTILITIES_HAS_EXEC_APP auto pathUtf8 = path.toUtf8(); auto argsUtf8 = QByteArrayList(); auto argsUtf8Array = std::vector(); if (args.isArray()) { const auto size = args.property(QStringLiteral("length")).toUInt(); argsUtf8.reserve(size); argsUtf8Array.reserve(static_cast(size) + 2); for (auto i = quint32(); i != size; ++i) { argsUtf8.append(args.property(i).toString().toUtf8()); } } argsUtf8Array.emplace_back(pathUtf8.data()); for (const auto &argUtf8 : argsUtf8) { argsUtf8Array.emplace_back(argUtf8.data()); } argsUtf8Array.emplace_back(nullptr); auto output = std::string(), errors = std::string(); try { auto exitStatus = CppUtilities::execHelperAppInSearchPath(pathUtf8.data(), argsUtf8Array.data(), output, errors, false, timeout); #ifndef CPP_UTILITIES_BOOST_PROCESS if (WIFEXITED(exitStatus)) { exitStatus = WEXITSTATUS(exitStatus); res.setProperty(QStringLiteral("status"), exitStatus); } #else res.setProperty(QStringLiteral("status"), exitStatus); #endif } catch (const std::runtime_error &e) { res.setProperty(QStringLiteral("error"), QString::fromUtf8(e.what())); } res.setProperty(QStringLiteral("stdout"), QString::fromStdString(output)); res.setProperty(QStringLiteral("stderr"), QString::fromStdString(errors)); #endif return res; } QString UtilityObject::formatName(const QString &str) const { return Utility::formatName(str); } QString UtilityObject::fixUmlauts(const QString &str) const { return Utility::fixUmlauts(str); } QJSValue UtilityObject::queryMusicBrainz(const QJSValue &songDescription) { return m_engine->newQObject(QtGui::queryMusicBrainz(makeSongDescription(songDescription))); } QJSValue UtilityObject::queryLyricsWikia(const QJSValue &songDescription) { return m_engine->newQObject(QtGui::queryLyricsWikia(makeSongDescription(songDescription))); } QJSValue UtilityObject::queryMakeItPersonal(const QJSValue &songDescription) { return m_engine->newQObject(QtGui::queryMakeItPersonal(makeSongDescription(songDescription))); } QJSValue UtilityObject::queryTekstowo(const QJSValue &songDescription) { return m_engine->newQObject(QtGui::queryTekstowo(makeSongDescription(songDescription))); } QByteArray UtilityObject::convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format) { auto image = QImage::fromData(imageData); if (image.isNull()) { return imageData; } if (!maxSize.isNull() && (image.width() > maxSize.width() || image.height() > maxSize.height())) { image = image.scaled(maxSize.width(), maxSize.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); if (image.isNull()) { return imageData; } } auto newData = QByteArray(); auto buffer = QBuffer(&newData); auto res = buffer.open(QIODevice::WriteOnly) && image.save(&buffer, format.isEmpty() ? "JPEG" : format.toUtf8().data()); return res ? newData : imageData; } static QString propertyString(const QJSValue &obj, const QString &propertyName) { const auto val = obj.property(propertyName); return val.isUndefined() || val.isNull() ? QString() : val.toString(); } QtGui::SongDescription UtilityObject::makeSongDescription(const QJSValue &obj) { auto desc = QtGui::SongDescription(); desc.songId = propertyString(obj, QStringLiteral("songId")); desc.title = propertyString(obj, QStringLiteral("title")); desc.album = propertyString(obj, QStringLiteral("album")); desc.albumId = propertyString(obj, QStringLiteral("albumId")); desc.artist = propertyString(obj, QStringLiteral("artist")); desc.artistId = propertyString(obj, QStringLiteral("artistId")); desc.year = propertyString(obj, QStringLiteral("year")); return desc; } PositionInSetObject::PositionInSetObject(TagParser::PositionInSet value, TagValueObject *relatedValue, QJSEngine *engine, QObject *parent) : QObject(parent) , m_v(value) , m_relatedValue(relatedValue) { Q_UNUSED(engine) } PositionInSetObject::PositionInSetObject(const PositionInSetObject &other) : QObject(other.parent()) , m_v(other.m_v) , m_relatedValue(nullptr) { } PositionInSetObject::~PositionInSetObject() { } qint32 PositionInSetObject::position() const { return m_v.position(); } void PositionInSetObject::setPosition(qint32 position) { if (m_relatedValue) { m_relatedValue->flagChange(); } m_v.setPosition(position); } qint32 PositionInSetObject::total() const { return m_v.total(); } void PositionInSetObject::setTotal(qint32 total) { if (m_relatedValue) { m_relatedValue->flagChange(); } m_v.setTotal(total); } QString PositionInSetObject::toString() const { return QString::fromStdString(m_v.toString()); } TagValueObject::TagValueObject(const TagParser::TagValue &value, QJSEngine *engine, QObject *parent) : QObject(parent) , m_engine(engine) , m_initial(true) { setContentFromTagValue(value); } TagValueObject::~TagValueObject() { } const QString &TagValueObject::type() const { return m_type; } const QJSValue &TagValueObject::content() const { return m_content; } const QJSValue &TagValueObject::initialContent() const { return m_initial ? m_content : m_initialContent; } void TagValueObject::setContent(const QJSValue &content) { flagChange(); m_content = content; } void TagValueObject::setContentFromTagValue(const TagParser::TagValue &value) { const auto type = TagParser::tagDataTypeString(value.type()); m_type = QString::fromUtf8(type.data(), Utility::sizeToInt(type.size())); switch (value.type()) { case TagParser::TagDataType::Undefined: break; case TagParser::TagDataType::Text: case TagParser::TagDataType::Popularity: case TagParser::TagDataType::DateTime: case TagParser::TagDataType::DateTimeExpression: case TagParser::TagDataType::TimeSpan: m_content = Utility::tagValueToQString(value); break; case TagParser::TagDataType::Integer: m_content = value.toInteger(); break; case TagParser::TagDataType::UnsignedInteger: if (auto v = value.toUnsignedInteger(); v < std::numeric_limits::max()) { m_content = QJSValue(static_cast(v)); } else { m_content = QString::number(v); } break; case TagParser::TagDataType::Binary: case TagParser::TagDataType::Picture: m_content = m_engine->toScriptValue(QByteArray(value.dataPointer(), Utility::sizeToInt(value.dataSize()))); break; case TagParser::TagDataType::PositionInSet: m_content = m_engine->newQObject(new PositionInSetObject(value.toPositionInSet(), this, m_engine, this)); break; default: m_content = QJSValue::NullValue; } } bool TagValueObject::isInitial() const { return m_initial; } TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const { auto res = TagParser::TagValue(); if (m_content.isUndefined() || m_content.isNull()) { return res; } if (const auto variant = m_content.toVariant(); variant.userType() == QMetaType::QByteArray) { const auto bytes = variant.toByteArray(); const auto container = TagParser::parseSignature(bytes.data(), static_cast(bytes.size())); res.assignData(bytes.data(), static_cast(bytes.size()), TagParser::TagDataType::Binary); res.setMimeType(TagParser::containerMimeType(container)); } else { const auto str = m_content.toString(); res.assignText(reinterpret_cast(str.utf16()), static_cast(str.size()) * (sizeof(ushort) / sizeof(char)), nativeUtf16Encoding, encoding); } return res; } void TagValueObject::flagChange() { if (m_initial) { if (const auto *const positionInSet = qobject_cast(m_content.toQObject())) { m_initialContent = m_engine->newQObject(new PositionInSetObject(*positionInSet)); } else { m_initialContent = m_content; } m_initial = false; } } void TagValueObject::restore() { if (!m_initial) { m_content = m_initialContent; m_initial = true; } } void TagValueObject::clear() { setContent(QJSValue()); } QString TagValueObject::toString() const { return m_content.toString(); } TagObject::TagObject(TagParser::Tag &tag, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent) : QObject(parent) , m_tag(tag) , m_diag(diag) , m_engine(engine) { } TagObject::~TagObject() { } QString TagObject::type() const { return Utility::qstr(m_tag.typeName()); } QJSValue TagObject::target() const { const auto &target = m_tag.target(); auto obj = m_engine->newObject(); obj.setProperty(QStringLiteral("level"), m_engine->toScriptValue(target.level())); obj.setProperty(QStringLiteral("levelName"), QJSValue(QString::fromStdString(target.levelName()))); obj.setProperty(QStringLiteral("tracks"), m_engine->toScriptValue(QList(target.tracks().cbegin(), target.tracks().cend()))); obj.setProperty(QStringLiteral("chapters"), m_engine->toScriptValue(QList(target.chapters().cbegin(), target.chapters().cend()))); obj.setProperty(QStringLiteral("editions"), m_engine->toScriptValue(QList(target.editions().cbegin(), target.editions().cend()))); obj.setProperty( QStringLiteral("attachments"), m_engine->toScriptValue(QList(target.attachments().cbegin(), target.attachments().cend()))); return obj; } QString TagObject::propertyNameForField(TagParser::KnownField field) { static const auto reverseMapping = [] { auto reverse = QHash(); for (const auto &mapping : FieldMapping::mapping()) { auto d = QString::fromUtf8(mapping.knownDenotation); if (d.isUpper()) { d = d.toLower(); // turn abbreviations into just lower case } else { d.front() = d.front().toLower(); } reverse[mapping.knownField] = std::move(d); } return reverse; }(); return reverseMapping.value(field, QString()); } std::string TagObject::printJsValue(const QJSValue &value) { const auto str = value.toString(); for (const auto c : str) { if (!c.isPrint()) { return "[binary]"; } } return str.toStdString(); } QJSValue &TagObject::fields() { if (!m_fields.isUndefined()) { return m_fields; } static const auto fieldRegex = QRegularExpression(QStringLiteral("\\s(\\w)")); m_fields = m_engine->newObject(); for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) { if (!m_tag.supportsField(field)) { continue; } if (const auto propertyName = propertyNameForField(field); !propertyName.isEmpty()) { const auto values = m_tag.values(field); const auto size = Utility::sizeToInt(values.size()); auto array = m_engine->newArray(size); for (auto i = quint32(); i != size; ++i) { array.setProperty(i, m_engine->newQObject(new TagValueObject(m_tag.value(field), m_engine, this))); } m_fields.setProperty(propertyName, array); } } return m_fields; } /// \brief Sets the first of the specified \a values as front-cover with no description replacing any existing cover values. template static void setId3v2CoverValues(TagType *tag, std::vector &&values) { auto &fields = tag->fields(); const auto id = tag->fieldId(TagParser::KnownField::Cover); const auto range = fields.equal_range(id); const auto first = range.first; constexpr auto coverType = CoverType(3); // assume front cover constexpr auto description = std::optional(); // assume no description for (auto &tagValue : values) { // check whether there is already a tag value with the current type and description auto pair = std::find_if(first, range.second, std::bind(&fieldPredicate, coverType, description, std::placeholders::_1)); if (pair != range.second) { // there is already a tag value with the current type and description // -> update this value pair->second.setValue(tagValue); // check whether there are more values with the current type and description while ((pair = std::find_if(++pair, range.second, std::bind(&fieldPredicate, coverType, description, std::placeholders::_1))) != range.second) { // -> remove these values as we only support one value of a type/description in the same tag pair->second.setValue(TagParser::TagValue()); } } else if (!tagValue.isEmpty()) { using FieldType = typename TagType::FieldType; auto newField = FieldType(id, tagValue); newField.setTypeInfo(static_cast(coverType)); fields.insert(std::pair(id, std::move(newField))); } break; // allow only setting one value for now } } void TagObject::applyChanges() { auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag ? CppUtilities::argsToString(m_tag.typeName(), " targeting ", m_tag.targetString()) : std::string(m_tag.typeName()); m_diag.emplace_back(TagParser::DiagLevel::Debug, "applying changes", std::move(context)); if (m_fields.isUndefined()) { return; } const auto encoding = m_tag.proposedTextEncoding(); for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) { if (!m_tag.supportsField(field)) { continue; } const auto propertyName = propertyNameForField(field); if (propertyName.isEmpty()) { continue; } auto propertyValue = m_fields.property(propertyName); if (!propertyValue.isArray()) { auto array = m_engine->newArray(1); array.setProperty(0, propertyValue); propertyValue = std::move(array); } const auto size = propertyValue.property(QStringLiteral("length")).toUInt(); const auto initialValues = m_tag.values(field); auto values = std::vector(); values.reserve(size); for (auto i = quint32(); i != size; ++i) { const auto arrayElement = propertyValue.property(i); if (arrayElement.isUndefined() || arrayElement.isNull()) { continue; } auto *tagValueObj = qobject_cast(arrayElement.toQObject()); if (!tagValueObj) { tagValueObj = new TagValueObject(i < initialValues.size() ? *initialValues[i] : TagParser::TagValue(), m_engine, this); tagValueObj->setContent(arrayElement); propertyValue.setProperty(i, m_engine->newQObject(tagValueObj)); } if (tagValueObj->isInitial()) { if (i < initialValues.size()) { values.emplace_back(*initialValues[i]); } continue; } auto &value = values.emplace_back(tagValueObj->toTagValue(encoding)); m_diag.emplace_back(TagParser::DiagLevel::Debug, value.isNull() ? CppUtilities::argsToString(" - delete ", propertyName.toStdString(), '[', i, ']') : (tagValueObj->initialContent().isUndefined() ? CppUtilities::argsToString( " - set ", propertyName.toStdString(), '[', i, "] to '", printJsValue(tagValueObj->content()), '\'') : ((tagValueObj->content().equals(tagValueObj->initialContent())) ? CppUtilities::argsToString(" - set ", propertyName.toStdString(), '[', i, "] to '", printJsValue(tagValueObj->content()), "\' (no change)") : CppUtilities::argsToString(" - change ", propertyName.toStdString(), '[', i, "] from '", printJsValue(tagValueObj->initialContent()), "' to '", printJsValue(tagValueObj->content()), '\''))), std::string()); } // assign cover values of ID3v2/VorbisComment tags as front-cover with no description if (field == TagParser::KnownField::Cover && !values.empty()) { switch (m_tag.type()) { case TagParser::TagType::Id3v2Tag: setId3v2CoverValues(static_cast(&m_tag), std::move(values)); continue; case TagParser::TagType::VorbisComment: setId3v2CoverValues(static_cast(&m_tag), std::move(values)); continue; default:; } } m_tag.setValues(field, values); } } MediaFileInfoObject::MediaFileInfoObject( TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent) : QObject(parent) , m_f(mediaFileInfo) , m_diag(diag) , m_engine(engine) , m_quiet(quiet) { } MediaFileInfoObject::MediaFileInfoObject( std::unique_ptr &&mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent) : MediaFileInfoObject(*mediaFileInfo.get(), diag, engine, quiet, parent) { m_f_owned = std::move(mediaFileInfo); } MediaFileInfoObject::~MediaFileInfoObject() { } QString MediaFileInfoObject::path() const { return QString::fromStdString(m_f.path()); } bool MediaFileInfoObject::isPathRelative() const { return QFileInfo(path()).isRelative(); } QString MediaFileInfoObject::name() const { return QString::fromStdString(m_f.fileName()); } QString MediaFileInfoObject::nameWithoutExtension() const { return QString::fromStdString(m_f.fileName(true)); } QString MediaFileInfoObject::extension() const { return QString::fromStdString(m_f.extension()); } QString MediaFileInfoObject::containingDirectory() const { return QString::fromStdString(m_f.containingDirectory()); } QString MediaFileInfoObject::savePath() const { return QtUtilities::fromNativeFileName(m_f.saveFilePath()); } void MediaFileInfoObject::setSavePath(const QString &path) { const auto nativePath = QtUtilities::toNativeFileName(path); m_f.setSaveFilePath(std::string_view(nativePath.data(), static_cast(nativePath.size()))); } QList &MediaFileInfoObject::tags() { if (!m_tags.isEmpty()) { return m_tags; } auto tags = m_f.tags(); m_tags.reserve(Utility::sizeToInt(tags.size())); for (auto *const tag : tags) { m_tags << new TagObject(*tag, m_diag, m_engine, this); } return m_tags; } void MediaFileInfoObject::applyChanges() { for (auto *const tag : m_tags) { tag->applyChanges(); } } bool MediaFileInfoObject::rename(const QString &newPath) { const auto from = m_f.path(); const auto fromNative = std::filesystem::path(CppUtilities::makeNativePath(from)); const auto toRelUtf8 = newPath.toUtf8(); const auto toRelView = CppUtilities::PathStringView(toRelUtf8.data(), static_cast(toRelUtf8.size())); const auto toNative = fromNative.parent_path().append(CppUtilities::makeNativePath(toRelView)); const auto toView = CppUtilities::extractNativePath(toNative.native()); try { m_f.stream().close(); std::filesystem::rename(fromNative, toNative); m_f.reportPathChanged(toView); m_f.stream().open(m_f.path(), std::ios_base::in | std::ios_base::out | std::ios_base::binary); } catch (const std::runtime_error &e) { m_diag.emplace_back(TagParser::DiagLevel::Critical, e.what(), CppUtilities::argsToString("renaming \"", from, "\" to \"", toView)); return false; } if (!m_quiet) { std::cout << " - Renamed \"" << from << "\" to \"" << toView << "\"\n"; } return true; } } // namespace Cli