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 }