1 /*
2     SPDX-FileCopyrightText: 2017 Nicolas Carion
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 #include "effectsrepository.hpp"
7 #include "core.h"
8 #include "kdenlivesettings.h"
9 #include "profiles/profilemodel.hpp"
10 #include "xml/xml.hpp"
11 
12 #include <KLocalizedString>
13 #include <QDir>
14 #include <QFile>
15 #include <QStandardPaths>
16 #include <QTextStream>
17 #include <mlt++/Mlt.h>
18 
19 std::unique_ptr<EffectsRepository> EffectsRepository::instance;
20 std::once_flag EffectsRepository::m_onceFlag;
21 
EffectsRepository()22 EffectsRepository::EffectsRepository()
23     : AbstractAssetsRepository<AssetListType::AssetType>()
24 {
25     init();
26     // Check that our favorite effects are valid
27     QStringList invalidEffect;
28     for (const QString &effect : KdenliveSettings::favorite_effects()) {
29         if (!exists(effect)) {
30             invalidEffect << effect;
31         }
32     }
33     if (!invalidEffect.isEmpty()) {
34         pCore->displayMessage(i18n("Some of your favorite effects are invalid and were removed: %1", invalidEffect.join(QLatin1Char(','))), ErrorMessage);
35         QStringList newFavorites = KdenliveSettings::favorite_effects();
36         for (const QString &effect : qAsConst(invalidEffect)) {
37             newFavorites.removeAll(effect);
38         }
39         KdenliveSettings::setFavorite_effects(newFavorites);
40     }
41 }
42 
retrieveListFromMlt() const43 Mlt::Properties *EffectsRepository::retrieveListFromMlt() const
44 {
45     return pCore->getMltRepository()->filters();
46 }
47 
getMetadata(const QString & effectId) const48 Mlt::Properties *EffectsRepository::getMetadata(const QString &effectId) const
49 {
50     return pCore->getMltRepository()->metadata(mlt_service_filter_type, effectId.toLatin1().data());
51 }
52 
parseCustomAssetFile(const QString & file_name,std::unordered_map<QString,Info> & customAssets) const53 void EffectsRepository::parseCustomAssetFile(const QString &file_name, std::unordered_map<QString, Info> &customAssets) const
54 {
55     QFile file(file_name);
56     QDomDocument doc;
57     doc.setContent(&file, false);
58     file.close();
59     QDomElement base = doc.documentElement();
60     if (base.tagName() == QLatin1String("effectgroup")) {
61         QDomNodeList effects = base.elementsByTagName(QStringLiteral("effect"));
62         if (effects.count() > 1) {
63             // Effect group
64             Info result;
65             result.xml = base;
66             result.description = Xml::getSubTagContent(base, QStringLiteral("description"));
67             for (int i = 0; i < effects.count(); ++i) {
68                 QDomNode currentNode = effects.item(i);
69                 if (currentNode.isNull()) {
70                     continue;
71                 }
72                 QDomElement currentEffect = currentNode.toElement();
73                 QString currentId = currentEffect.attribute(QStringLiteral("id"), QString());
74                 if (currentId.isEmpty()) {
75                     currentId = currentEffect.attribute(QStringLiteral("tag"), QString());
76                 }
77                 if (!exists(currentId) && customAssets.count(currentId) == 0) {
78                     qWarning() << "unsupported effect in group" << currentId << ":" << file_name;
79                     return;
80                 }
81             }
82             QString type = base.attribute(QStringLiteral("type"), QString());
83             if (type == QLatin1String("customAudio")) {
84                 result.type = AssetListType::AssetType::CustomAudio;
85             } else {
86                 result.type = AssetListType::AssetType::Custom;
87             }
88             result.id = base.attribute(QStringLiteral("id"), QString());
89             if (result.id.isEmpty()) {
90                 result.id = QFileInfo(file_name).baseName();
91             }
92             if (!result.id.isEmpty()) {
93                 result.name = result.id;
94                 customAssets[result.id] = result;
95             }
96             return;
97         }
98     }
99     QDomNodeList effects = doc.elementsByTagName(QStringLiteral("effect"));
100     int nbr_effect = effects.count();
101     if (nbr_effect == 0) {
102         qWarning() << "broken effect:" << file_name;
103         return;
104     }
105 
106     for (int i = 0; i < nbr_effect; ++i) {
107         QDomNode currentNode = effects.item(i);
108         if (currentNode.isNull()) {
109             continue;
110         }
111         QDomElement currentEffect = currentNode.toElement();
112         Info result;
113         bool ok = parseInfoFromXml(currentEffect, result);
114         if (!ok) {
115             continue;
116         }
117 
118         if (customAssets.count(result.id) > 0) {
119             //qDebug() << "duplicate effect" << result.id;
120         }
121 
122         result.xml = currentEffect;
123 
124         // Parse type information.
125         // Video effect by default
126         result.type = AssetListType::AssetType::Video;
127         QString type = currentEffect.attribute(QStringLiteral("type"), QString());
128         if (type == QLatin1String("audio")) {
129             result.type = AssetListType::AssetType::Audio;
130         } else if (type == QLatin1String("customVideo")) {
131             result.type = AssetListType::AssetType::Custom;
132         } else if (type == QLatin1String("customAudio")) {
133             result.type = AssetListType::AssetType::CustomAudio;
134         } else if (type == QLatin1String("hidden")) {
135             result.type = AssetListType::AssetType::Hidden;
136         } else if (type == QLatin1String("custom")) {
137             // Old type effect, update to customVideo / customAudio
138             const QString effectTag = currentEffect.attribute(QStringLiteral("tag"));
139             QScopedPointer<Mlt::Properties> metadata(getMetadata(effectTag));
140             if (metadata && metadata->is_valid()) {
141                 Mlt::Properties tags(mlt_properties(metadata->get_data("tags")));
142                 if (QString(tags.get(0)) == QLatin1String("Audio")) {
143                     result.type = AssetListType::AssetType::CustomAudio;
144                     currentEffect.setAttribute(QStringLiteral("type"), QStringLiteral("customAudio"));
145                 } else {
146                     result.type = AssetListType::AssetType::Custom;
147                     currentEffect.setAttribute(QStringLiteral("type"), QStringLiteral("customVideo"));
148                 }
149                 QFile effectFile(file_name);
150                 if (effectFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
151                     effectFile.write(doc.toString().toUtf8());
152                 }
153                 file.close();
154             }
155         } else if (type == QLatin1String("text")) {
156             result.type = AssetListType::AssetType::Text;
157         }
158         customAssets[result.id] = result;
159     }
160 }
161 
get()162 std::unique_ptr<EffectsRepository> &EffectsRepository::get()
163 {
164     std::call_once(m_onceFlag, [] { instance.reset(new EffectsRepository()); });
165     return instance;
166 }
167 
assetDirs() const168 QStringList EffectsRepository::assetDirs() const
169 {
170     return QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory);
171 }
172 
parseType(QScopedPointer<Mlt::Properties> & metadata,Info & res)173 void EffectsRepository::parseType(QScopedPointer<Mlt::Properties> &metadata, Info &res)
174 {
175     res.type = AssetListType::AssetType::Video;
176     Mlt::Properties tags(mlt_properties(metadata->get_data("tags")));
177     if (QString(tags.get(0)) == QLatin1String("Audio")) {
178         res.type = AssetListType::AssetType::Audio;
179     }
180 }
181 
assetBlackListPath() const182 QString EffectsRepository::assetBlackListPath() const
183 {
184     return QStringLiteral(":data/blacklisted_effects.txt");
185 }
186 
assetPreferredListPath() const187 QString EffectsRepository::assetPreferredListPath() const
188 {
189     return QStringLiteral(":data/preferred_effects.txt");
190 }
191 
isPreferred(const QString & effectId) const192 bool EffectsRepository::isPreferred(const QString &effectId) const
193 {
194     return m_preferred_list.contains(effectId);
195 }
196 
getEffect(const QString & effectId) const197 std::unique_ptr<Mlt::Filter> EffectsRepository::getEffect(const QString &effectId) const
198 {
199     Q_ASSERT(exists(effectId));
200     QString service_name = m_assets.at(effectId).mltId;
201     // We create the Mlt element from its name
202     auto filter = std::make_unique<Mlt::Filter>(pCore->getCurrentProfile()->profile(), service_name.toLatin1().constData(), nullptr);
203     return filter;
204 }
205 
hasInternalEffect(const QString & effectId) const206 bool EffectsRepository::hasInternalEffect(const QString &effectId) const
207 {
208     // Retrieve the list of MLT's available assets.
209     QScopedPointer<Mlt::Properties> assets(retrieveListFromMlt());
210     int max = assets->count();
211     for (int i = 0; i < max; ++i) {
212         if (assets->get_name(i) == effectId) {
213             return true;
214         }
215     }
216     return false;
217 }
218 
getCustomPath(const QString & id)219 QString EffectsRepository::getCustomPath(const QString &id)
220 {
221     QString customAssetDir = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory);
222     QPair <QStringList, QStringList> results;
223     QDir current_dir(customAssetDir);
224     return current_dir.absoluteFilePath(QString("%1.xml").arg(id));
225 }
226 
227 
reloadCustom(const QString & path)228 QPair<QString, QString> EffectsRepository::reloadCustom(const QString &path)
229 {
230     std::unordered_map<QString, Info> customAssets;
231     parseCustomAssetFile(path, customAssets);
232     QPair<QString, QString> result;
233     // TODO: handle files with several effects
234     for (const auto &custom : customAssets) {
235         // Custom assets should override default ones
236         m_assets[custom.first] = custom.second;
237         result.first = custom.first;
238         result.second = custom.second.mltId;
239     }
240     return result;
241 }
242 
isGroup(const QString & assetId) const243 bool EffectsRepository::isGroup(const QString &assetId) const
244 {
245     if (m_assets.count(assetId) > 0) {
246         QDomElement xml = m_assets.at(assetId).xml;
247         if (xml.tagName() == QLatin1String("effectgroup")) {
248             return true;
249         }
250     }
251     return false;
252 }
253 
254 
fixDeprecatedEffects()255 QPair <QStringList, QStringList> EffectsRepository::fixDeprecatedEffects()
256 {
257     QString customAssetDir = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory);
258     QPair <QStringList, QStringList> results;
259     QDir current_dir(customAssetDir);
260     QStringList filter;
261     filter << QStringLiteral("*.xml");
262     QStringList fileList = current_dir.entryList(filter, QDir::Files);
263     QStringList failed;
264     for (const auto &file : qAsConst(fileList)) {
265         QString path = current_dir.absoluteFilePath(file);
266         QPair <QString, QString> fixResult = fixCustomAssetFile(path);
267         if (!fixResult.first.isEmpty()) {
268             results.first << fixResult.first;
269         } else if (!fixResult.second.isEmpty()) {
270             results.second << fixResult.second;
271         }
272     }
273     return results;
274 }
275 
fixCustomAssetFile(const QString & path)276 QPair <QString, QString> EffectsRepository::fixCustomAssetFile(const QString &path)
277 {
278     QPair <QString, QString> results;
279     QFile file(path);
280     QDomDocument doc;
281     doc.setContent(&file, false);
282     file.close();
283     QDomElement base = doc.documentElement();
284     if (base.tagName() == QLatin1String("effectgroup")) {
285         // Groups not implemented
286         return results;
287     }
288     QDomNodeList effects = doc.elementsByTagName(QStringLiteral("effect"));
289 
290     int nbr_effect = effects.count();
291     if (nbr_effect == 0) {
292         qWarning() << "broken effect:" << path;
293         results.second = path;
294         return results;
295     }
296     bool effectAdjusted = false;
297     for (int i = 0; i < nbr_effect; ++i) {
298         QDomNode currentNode = effects.item(i);
299         if (currentNode.isNull()) {
300             continue;
301         }
302         QDomElement currentEffect = currentNode.toElement();
303         Info result;
304         bool ok = parseInfoFromXml(currentEffect, result);
305         if (!ok) {
306             continue;
307         }
308         if (currentEffect.hasAttribute(QLatin1String("kdenlive_info"))) {
309             // This is a pre 19.x custom effect, adjust param values
310             // First backup effect in legacy folder
311             QDir dir(QFileInfo(path).absoluteDir());
312             if (!dir.mkpath(QStringLiteral("legacy"))) {
313                 // Cannot create the legacy folder, abort
314                 qWarning() << "Could not create old effects backup folder" << dir.absolutePath();
315                 results.second = path;
316                 return results;
317             }
318             currentEffect.removeAttribute(QLatin1String("kdenlive_info"));
319             effectAdjusted = true;
320             QDomNodeList params = currentEffect.elementsByTagName(QLatin1String("parameter"));
321             for (int j = 0; j < params.count(); ++j) {
322                 QDomNode node = params.item(j);
323                 if (node.isNull()) {
324                     continue;
325                 }
326                 QDomElement param = node.toElement();
327                 if (param.hasAttribute(QLatin1String("factor")) && (param.attribute(QLatin1String("type")) == QLatin1String("simplekeyframe") || param.attribute(QLatin1String("type")) == QLatin1String("animated"))) {
328                     // This is an old style effect, adjust current and default values
329                     QString currentValue;
330                     if (!param.hasAttribute(QLatin1String("value"))) {
331                         currentValue = param.attribute(QLatin1String("keyframes"));
332                     } else {
333                         currentValue = param.attribute(QLatin1String("value"));
334                     }
335                     ok = false;
336                     int factor = param.attribute(QLatin1String("factor")).toInt(&ok);
337                     if (ok) {
338                         double defaultVal = param.attribute(QLatin1String("default")).toDouble() / factor;
339                         param.setAttribute(QLatin1String("default"), QString::number(defaultVal));
340                         if (currentValue.contains(QLatin1Char('='))) {
341                             QStringList valueStr = currentValue.split(QLatin1Char(';'));
342                             QStringList resultStr;
343                             for (const QString &val : qAsConst(valueStr)) {
344                                 if (val.contains(QLatin1Char('='))) {
345                                     QString frame = val.section(QLatin1Char('='), 0, 0);
346                                     QString frameVal = val.section(QLatin1Char('='), 1);
347                                     double v = frameVal.toDouble() / factor;
348                                     resultStr << QString("%1=%2").arg(frame).arg(v);
349                                 } else {
350                                     double v = val.toDouble() / factor;
351                                     resultStr << QString::number(v);
352                                 }
353                             }
354                             param.setAttribute(QLatin1String("value"), resultStr.join(QLatin1Char(';')));
355                         }
356                     }
357                 }
358             }
359         }
360         result.xml = currentEffect;
361     }
362     if (effectAdjusted) {
363         QDir dir(QFileInfo(path).absoluteDir());
364         dir.cd(QStringLiteral("legacy"));
365         if (!file.copy(dir.absoluteFilePath(QFileInfo(file).fileName()))) {
366             // Cannot copy the backup file
367             qWarning() << "Could not copy old effect to" << dir.absoluteFilePath(QFileInfo(file).fileName());
368             results.second = path;
369             return results;
370         }
371         if (file.open(QFile::WriteOnly | QFile::Truncate)) {
372             QTextStream out(&file);
373             out.setCodec("UTF-8");
374             out << doc.toString();
375         }
376         file.close();
377         results.first = path;
378     }
379     return results;
380 }
381 
deleteEffect(const QString & id)382 void EffectsRepository::deleteEffect(const QString &id)
383 {
384     if (!exists(id)) {
385         return;
386     }
387     QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/"));
388     QFile file(dir.absoluteFilePath(id + QStringLiteral(".xml")));
389     if (file.exists()) {
390         file.remove();
391         m_assets.erase(id);
392     }
393 }
394 
isAudioEffect(const QString & assetId) const395 bool EffectsRepository::isAudioEffect(const QString &assetId) const
396 {
397     if (m_assets.count(assetId) > 0) {
398         AssetListType::AssetType type = m_assets.at(assetId).type;
399         return type == AssetListType::AssetType::Audio || type == AssetListType::AssetType::CustomAudio;
400     }
401     return false;
402 }
403