File indexing completed on 2024-04-14 04:46:35
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "effectsrepository.hpp" 0007 #include "core.h" 0008 #include "kdenlivesettings.h" 0009 #include "profiles/profilemodel.hpp" 0010 #include "xml/xml.hpp" 0011 0012 #include <QApplication> 0013 #include <QDir> 0014 #include <QFile> 0015 #include <QStandardPaths> 0016 #include <QTextStream> 0017 0018 #include <KLocalizedString> 0019 #include <KMessageBox> 0020 0021 #include <mlt++/Mlt.h> 0022 0023 std::unique_ptr<EffectsRepository> EffectsRepository::instance; 0024 std::once_flag EffectsRepository::m_onceFlag; 0025 0026 EffectsRepository::EffectsRepository() 0027 : AbstractAssetsRepository<AssetListType::AssetType>() 0028 { 0029 init(); 0030 // Check that our favorite effects are valid 0031 QStringList invalidEffect; 0032 for (const QString &effect : KdenliveSettings::favorite_effects()) { 0033 if (!exists(effect)) { 0034 invalidEffect << effect; 0035 } 0036 } 0037 if (!invalidEffect.isEmpty()) { 0038 pCore->displayMessage(i18n("Some of your favorite effects are invalid and were removed: %1", invalidEffect.join(QLatin1Char(','))), ErrorMessage); 0039 QStringList newFavorites = KdenliveSettings::favorite_effects(); 0040 for (const QString &effect : qAsConst(invalidEffect)) { 0041 newFavorites.removeAll(effect); 0042 } 0043 KdenliveSettings::setFavorite_effects(newFavorites); 0044 } 0045 } 0046 0047 Mlt::Properties *EffectsRepository::retrieveListFromMlt() const 0048 { 0049 return pCore->getMltRepository()->filters(); 0050 } 0051 0052 Mlt::Properties *EffectsRepository::getMetadata(const QString &effectId) const 0053 { 0054 return pCore->getMltRepository()->metadata(mlt_service_filter_type, effectId.toLatin1().data()); 0055 } 0056 0057 void EffectsRepository::parseCustomAssetFile(const QString &file_name, std::unordered_map<QString, Info> &customAssets) const 0058 { 0059 QDomDocument doc; 0060 if (!Xml::docContentFromFile(doc, file_name, false)) { 0061 return; 0062 } 0063 0064 QDomElement base = doc.documentElement(); 0065 if (base.tagName() == QLatin1String("effectgroup")) { 0066 QDomNodeList effects = base.elementsByTagName(QStringLiteral("effect")); 0067 if (effects.count() > 1) { 0068 // Effect group 0069 Info result; 0070 result.xml = base; 0071 result.description = Xml::getSubTagContent(base, QStringLiteral("description")); 0072 for (int i = 0; i < effects.count(); ++i) { 0073 QDomNode currentNode = effects.item(i); 0074 if (currentNode.isNull()) { 0075 continue; 0076 } 0077 QDomElement currentEffect = currentNode.toElement(); 0078 QString currentId = currentEffect.attribute(QStringLiteral("id"), QString()); 0079 if (currentId.isEmpty()) { 0080 currentId = currentEffect.attribute(QStringLiteral("tag"), QString()); 0081 } 0082 if (!exists(currentId) && customAssets.count(currentId) == 0) { 0083 qWarning() << "unsupported effect in group" << currentId << ":" << file_name; 0084 return; 0085 } 0086 } 0087 QString type = base.attribute(QStringLiteral("type"), QString()); 0088 if (type == QLatin1String("customAudio")) { 0089 if (file_name.contains(QStringLiteral("effect-templates"))) { 0090 result.type = AssetListType::AssetType::TemplateAudio; 0091 } else { 0092 result.type = AssetListType::AssetType::CustomAudio; 0093 } 0094 } else { 0095 if (file_name.contains(QStringLiteral("effect-templates"))) { 0096 result.type = AssetListType::AssetType::Template; 0097 } else { 0098 result.type = AssetListType::AssetType::Custom; 0099 } 0100 } 0101 result.id = base.attribute(QStringLiteral("id"), QString()); 0102 if (result.id.isEmpty()) { 0103 result.id = QFileInfo(file_name).baseName(); 0104 } 0105 if (!result.id.isEmpty()) { 0106 result.name = result.description; 0107 customAssets[result.id] = result; 0108 } 0109 return; 0110 } 0111 } 0112 QDomNodeList effects = doc.elementsByTagName(QStringLiteral("effect")); 0113 int nbr_effect = effects.count(); 0114 if (nbr_effect == 0) { 0115 qWarning() << "broken effect:" << file_name; 0116 return; 0117 } 0118 0119 for (int i = 0; i < nbr_effect; ++i) { 0120 QDomNode currentNode = effects.item(i); 0121 if (currentNode.isNull()) { 0122 continue; 0123 } 0124 QDomElement currentEffect = currentNode.toElement(); 0125 Info result; 0126 bool ok = parseInfoFromXml(currentEffect, result); 0127 if (!ok) { 0128 continue; 0129 } 0130 0131 if (customAssets.count(result.id) > 0) { 0132 // qDebug() << "duplicate effect" << result.id << ", VERSION= "<<result.version<<", EXISTING: "<<customAssets.at(result.id).version; 0133 if (result.version < customAssets.at(result.id).version) { 0134 continue; 0135 } 0136 } 0137 0138 result.xml = currentEffect; 0139 0140 // Parse type information. 0141 // Video effect by default 0142 result.type = AssetListType::AssetType::Video; 0143 QString type = currentEffect.attribute(QStringLiteral("type"), QString()); 0144 if (type == QLatin1String("audio")) { 0145 result.type = AssetListType::AssetType::Audio; 0146 } else if (type == QLatin1String("customVideo")) { 0147 result.type = AssetListType::AssetType::Custom; 0148 } else if (type == QLatin1String("customAudio")) { 0149 result.type = AssetListType::AssetType::CustomAudio; 0150 } else if (type == QLatin1String("hidden")) { 0151 result.type = AssetListType::AssetType::Hidden; 0152 } else if (type == QLatin1String("custom")) { 0153 // Old type effect, update to customVideo / customAudio 0154 const QString effectTag = currentEffect.attribute(QStringLiteral("tag")); 0155 std::unique_ptr<Mlt::Properties> metadata(getMetadata(effectTag)); 0156 if (metadata && metadata->is_valid()) { 0157 Mlt::Properties tags(mlt_properties(metadata->get_data("tags"))); 0158 if (QString(tags.get(0)) == QLatin1String("Audio")) { 0159 result.type = AssetListType::AssetType::CustomAudio; 0160 currentEffect.setAttribute(QStringLiteral("type"), QStringLiteral("customAudio")); 0161 } else { 0162 result.type = AssetListType::AssetType::Custom; 0163 currentEffect.setAttribute(QStringLiteral("type"), QStringLiteral("customVideo")); 0164 } 0165 Xml::docContentToFile(doc, file_name); 0166 } 0167 } else if (type == QLatin1String("text")) { 0168 result.type = AssetListType::AssetType::Text; 0169 } 0170 customAssets[result.id] = result; 0171 } 0172 } 0173 0174 std::unique_ptr<EffectsRepository> &EffectsRepository::get() 0175 { 0176 std::call_once(m_onceFlag, [] { instance.reset(new EffectsRepository()); }); 0177 return instance; 0178 } 0179 0180 QStringList EffectsRepository::assetDirs() const 0181 { 0182 QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("effect-templates"), QStandardPaths::LocateDirectory); 0183 dirs.append(QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory)); 0184 return dirs; 0185 } 0186 0187 void EffectsRepository::parseType(Mlt::Properties *metadata, Info &res) 0188 { 0189 res.type = AssetListType::AssetType::Video; 0190 Mlt::Properties tags(mlt_properties(metadata->get_data("tags"))); 0191 if (QString(tags.get(0)) == QLatin1String("Audio")) { 0192 res.type = AssetListType::AssetType::Audio; 0193 } 0194 } 0195 0196 QString EffectsRepository::assetBlackListPath() const 0197 { 0198 return QStringLiteral(":data/blacklisted_effects.txt"); 0199 } 0200 0201 QString EffectsRepository::assetPreferredListPath() const 0202 { 0203 return QStringLiteral(":data/preferred_effects.txt"); 0204 } 0205 0206 bool EffectsRepository::isPreferred(const QString &effectId) const 0207 { 0208 return m_preferred_list.contains(effectId); 0209 } 0210 0211 std::unique_ptr<Mlt::Filter> EffectsRepository::getEffect(const QString &effectId) const 0212 { 0213 Q_ASSERT(exists(effectId)); 0214 QString service_name = m_assets.at(effectId).mltId; 0215 // We create the Mlt element from its name 0216 auto filter = std::make_unique<Mlt::Filter>(pCore->getProjectProfile(), service_name.toLatin1().constData(), nullptr); 0217 return filter; 0218 } 0219 0220 bool EffectsRepository::hasInternalEffect(const QString &effectId) const 0221 { 0222 // Retrieve the list of MLT's available assets. 0223 QScopedPointer<Mlt::Properties> assets(retrieveListFromMlt()); 0224 int max = assets->count(); 0225 for (int i = 0; i < max; ++i) { 0226 if (assets->get_name(i) == effectId) { 0227 return true; 0228 } 0229 } 0230 return false; 0231 } 0232 0233 QString EffectsRepository::getCustomPath(const QString &id) 0234 { 0235 QString customAssetDir = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory); 0236 QPair<QStringList, QStringList> results; 0237 QDir current_dir(customAssetDir); 0238 return current_dir.absoluteFilePath(QString("%1.xml").arg(id)); 0239 } 0240 0241 QPair<QString, QString> EffectsRepository::reloadCustom(const QString &path) 0242 { 0243 std::unordered_map<QString, Info> customAssets; 0244 parseCustomAssetFile(path, customAssets); 0245 QPair<QString, QString> result; 0246 // TODO: handle files with several effects 0247 for (const auto &custom : customAssets) { 0248 // Custom assets should override default ones 0249 m_assets[custom.first] = custom.second; 0250 result.first = custom.first; 0251 result.second = custom.second.mltId; 0252 } 0253 return result; 0254 } 0255 0256 bool EffectsRepository::isGroup(const QString &assetId) const 0257 { 0258 if (m_assets.count(assetId) > 0) { 0259 QDomElement xml = m_assets.at(assetId).xml; 0260 if (xml.tagName() == QLatin1String("effectgroup")) { 0261 return true; 0262 } 0263 } 0264 return false; 0265 } 0266 0267 QPair<QStringList, QStringList> EffectsRepository::fixDeprecatedEffects() 0268 { 0269 QString customAssetDir = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory); 0270 QPair<QStringList, QStringList> results; 0271 QDir current_dir(customAssetDir); 0272 QStringList filter; 0273 filter << QStringLiteral("*.xml"); 0274 QStringList fileList = current_dir.entryList(filter, QDir::Files); 0275 QStringList failed; 0276 for (const auto &file : qAsConst(fileList)) { 0277 QString path = current_dir.absoluteFilePath(file); 0278 QPair<QString, QString> fixResult = fixCustomAssetFile(path); 0279 if (!fixResult.first.isEmpty()) { 0280 results.first << fixResult.first; 0281 } else if (!fixResult.second.isEmpty()) { 0282 results.second << fixResult.second; 0283 } 0284 } 0285 return results; 0286 } 0287 0288 QPair<QString, QString> EffectsRepository::fixCustomAssetFile(const QString &path) 0289 { 0290 QPair<QString, QString> results; 0291 QDomDocument doc; 0292 if (!Xml::docContentFromFile(doc, path, false)) { 0293 return results; 0294 } 0295 0296 QDomElement base = doc.documentElement(); 0297 if (base.tagName() == QLatin1String("effectgroup")) { 0298 // Groups not implemented 0299 return results; 0300 } 0301 QDomNodeList effects = doc.elementsByTagName(QStringLiteral("effect")); 0302 0303 int nbr_effect = effects.count(); 0304 if (nbr_effect == 0) { 0305 qWarning() << "broken effect:" << path; 0306 results.second = path; 0307 return results; 0308 } 0309 bool effectAdjusted = false; 0310 for (int i = 0; i < nbr_effect; ++i) { 0311 QDomNode currentNode = effects.item(i); 0312 if (currentNode.isNull()) { 0313 continue; 0314 } 0315 QDomElement currentEffect = currentNode.toElement(); 0316 Info result; 0317 bool ok = parseInfoFromXml(currentEffect, result); 0318 if (!ok) { 0319 continue; 0320 } 0321 if (currentEffect.hasAttribute(QLatin1String("kdenlive_info"))) { 0322 // This is a pre 19.x custom effect, adjust param values 0323 // First backup effect in legacy folder 0324 QDir dir(QFileInfo(path).absoluteDir()); 0325 if (!dir.mkpath(QStringLiteral("legacy"))) { 0326 // Cannot create the legacy folder, abort 0327 qWarning() << "Could not create old effects backup folder" << dir.absolutePath(); 0328 results.second = path; 0329 return results; 0330 } 0331 currentEffect.removeAttribute(QLatin1String("kdenlive_info")); 0332 effectAdjusted = true; 0333 QDomNodeList params = currentEffect.elementsByTagName(QLatin1String("parameter")); 0334 for (int j = 0; j < params.count(); ++j) { 0335 QDomNode node = params.item(j); 0336 if (node.isNull()) { 0337 continue; 0338 } 0339 QDomElement param = node.toElement(); 0340 if (param.hasAttribute(QLatin1String("factor")) && (param.attribute(QLatin1String("type")) == QLatin1String("simplekeyframe") || 0341 param.attribute(QLatin1String("type")) == QLatin1String("animated"))) { 0342 // This is an old style effect, adjust current and default values 0343 QString currentValue; 0344 if (!param.hasAttribute(QLatin1String("value"))) { 0345 currentValue = param.attribute(QLatin1String("keyframes")); 0346 } else { 0347 currentValue = param.attribute(QLatin1String("value")); 0348 } 0349 ok = false; 0350 int factor = param.attribute(QLatin1String("factor")).toInt(&ok); 0351 if (ok) { 0352 double defaultVal = param.attribute(QLatin1String("default")).toDouble() / factor; 0353 param.setAttribute(QLatin1String("default"), QString::number(defaultVal)); 0354 if (currentValue.contains(QLatin1Char('='))) { 0355 QStringList valueStr = currentValue.split(QLatin1Char(';')); 0356 QStringList resultStr; 0357 for (const QString &val : qAsConst(valueStr)) { 0358 if (val.contains(QLatin1Char('='))) { 0359 QString frame = val.section(QLatin1Char('='), 0, 0); 0360 QString frameVal = val.section(QLatin1Char('='), 1); 0361 double v = frameVal.toDouble() / factor; 0362 resultStr << QString("%1=%2").arg(frame).arg(v); 0363 } else { 0364 double v = val.toDouble() / factor; 0365 resultStr << QString::number(v); 0366 } 0367 } 0368 param.setAttribute(QLatin1String("value"), resultStr.join(QLatin1Char(';'))); 0369 } 0370 } 0371 } 0372 } 0373 } 0374 result.xml = currentEffect; 0375 } 0376 if (effectAdjusted) { 0377 QDir dir(QFileInfo(path).absoluteDir()); 0378 dir.cd(QStringLiteral("legacy")); 0379 QFile file(path); 0380 if (!file.copy(dir.absoluteFilePath(QFileInfo(file).fileName()))) { 0381 // Cannot copy the backup file 0382 qWarning() << "Could not copy old effect to" << dir.absoluteFilePath(QFileInfo(file).fileName()); 0383 results.second = path; 0384 return results; 0385 } 0386 if (file.open(QFile::WriteOnly | QFile::Truncate)) { 0387 QTextStream out(&file); 0388 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0389 out.setCodec("UTF-8"); 0390 #endif 0391 out << doc.toString(); 0392 } else { 0393 KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", file.fileName())); 0394 } 0395 file.close(); 0396 results.first = path; 0397 } 0398 return results; 0399 } 0400 0401 void EffectsRepository::deleteEffect(const QString &id) 0402 { 0403 if (!exists(id)) { 0404 return; 0405 } 0406 QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/")); 0407 QFile file(dir.absoluteFilePath(id + QStringLiteral(".xml"))); 0408 if (file.exists()) { 0409 file.remove(); 0410 m_assets.erase(id); 0411 } 0412 } 0413 0414 bool EffectsRepository::isAudioEffect(const QString &assetId) const 0415 { 0416 if (m_assets.count(assetId) > 0) { 0417 AssetListType::AssetType type = m_assets.at(assetId).type; 0418 return type == AssetListType::AssetType::Audio || type == AssetListType::AssetType::CustomAudio || type == AssetListType::AssetType::TemplateAudio; 0419 } 0420 return false; 0421 } 0422 0423 bool EffectsRepository::isTextEffect(const QString &assetId) const 0424 { 0425 if (m_assets.count(assetId) > 0) { 0426 if (m_assets.at(assetId).type == AssetListType::AssetType::Text) { 0427 return true; 0428 } 0429 } 0430 return false; 0431 }