EffectFileHandler.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. #include <effectengine/EffectFileHandler.h>
  2. // util
  3. #include <utils/JsonUtils.h>
  4. // qt
  5. #include <QJsonArray>
  6. #include <QFileInfo>
  7. #include <QDir>
  8. #include <QMap>
  9. #include <QByteArray>
  10. EffectFileHandler* EffectFileHandler::efhInstance;
  11. EffectFileHandler::EffectFileHandler(const QString& rootPath, const QJsonDocument& effectConfig, QObject* parent)
  12. : QObject(parent)
  13. , _log(Logger::getInstance("EFFECTFILES"))
  14. , _rootPath(rootPath)
  15. {
  16. EffectFileHandler::efhInstance = this;
  17. Q_INIT_RESOURCE(EffectEngine);
  18. // init
  19. handleSettingsUpdate(settings::EFFECTS, effectConfig);
  20. }
  21. void EffectFileHandler::handleSettingsUpdate(settings::type type, const QJsonDocument& config)
  22. {
  23. if (type == settings::EFFECTS)
  24. {
  25. _effectConfig = config.object();
  26. // update effects and schemas
  27. updateEffects();
  28. }
  29. }
  30. QString EffectFileHandler::deleteEffect(const QString& effectName)
  31. {
  32. QString resultMsg;
  33. std::list<EffectDefinition> effectsDefinition = getEffects();
  34. std::list<EffectDefinition>::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(),
  35. [&effectName](const EffectDefinition& effectDefinition) {return effectDefinition.name == effectName; }
  36. );
  37. if (it != effectsDefinition.end())
  38. {
  39. QFileInfo effectConfigurationFile(it->file);
  40. if (!effectConfigurationFile.absoluteFilePath().startsWith(':'))
  41. {
  42. if (effectConfigurationFile.exists())
  43. {
  44. if ((it->script == ":/effects/gif.py") && !it->args.value("file").toString("").isEmpty())
  45. {
  46. QFileInfo effectImageFile(it->args.value("file").toString());
  47. if (effectImageFile.exists())
  48. {
  49. QFile::remove(effectImageFile.absoluteFilePath());
  50. }
  51. }
  52. bool result = QFile::remove(effectConfigurationFile.absoluteFilePath());
  53. if (result)
  54. {
  55. updateEffects();
  56. resultMsg = "";
  57. }
  58. else
  59. {
  60. resultMsg = "Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions";
  61. }
  62. }
  63. else
  64. {
  65. resultMsg = "Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath();
  66. }
  67. }
  68. else
  69. {
  70. resultMsg = "Can't delete internal effect: " + effectName;
  71. }
  72. }
  73. else
  74. {
  75. resultMsg = "Effect " + effectName + " not found";
  76. }
  77. return resultMsg;
  78. }
  79. QString EffectFileHandler::saveEffect(const QJsonObject& message)
  80. {
  81. QString resultMsg;
  82. if (!message["args"].toObject().isEmpty())
  83. {
  84. QString scriptName = message["script"].toString();
  85. std::list<EffectSchema> effectsSchemas = getEffectSchemas();
  86. std::list<EffectSchema>::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(),
  87. [&scriptName](const EffectSchema& schema) {return schema.pyFile == scriptName; }
  88. );
  89. if (it != effectsSchemas.end())
  90. {
  91. if (!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log))
  92. {
  93. return "Error during arg validation against schema, please consult the Hyperion Log";
  94. }
  95. QJsonObject effectJson;
  96. QJsonArray effectArray;
  97. effectArray = _effectConfig["paths"].toArray();
  98. if (!effectArray.empty())
  99. {
  100. QString effectName = message["name"].toString();
  101. if (effectName.trimmed().isEmpty() || effectName.trimmed().startsWith(":"))
  102. {
  103. return "Can't save new effect. Effect name is empty or begins with a dot.";
  104. }
  105. effectJson["name"] = effectName;
  106. effectJson["script"] = message["script"].toString();
  107. effectJson["args"] = message["args"].toObject();
  108. std::list<EffectDefinition> availableEffects = getEffects();
  109. std::list<EffectDefinition>::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(),
  110. [&effectName](const EffectDefinition& effectDefinition) {return effectDefinition.name == effectName; }
  111. );
  112. QFileInfo newFileName;
  113. if (iter != availableEffects.end())
  114. {
  115. newFileName.setFile(iter->file);
  116. if (newFileName.absoluteFilePath().startsWith(':'))
  117. {
  118. return "The effect name '" + effectName + "' is assigned to an internal effect. Please rename your effect.";
  119. }
  120. }
  121. else
  122. {
  123. QString f = effectArray[0].toString().replace("$ROOT", _rootPath) + '/' + effectName.replace(QString(" "), QString("")) + QString(".json");
  124. newFileName.setFile(f);
  125. }
  126. if (!message["imageData"].toString("").isEmpty() && !message["args"].toObject().value("file").toString("").isEmpty())
  127. {
  128. QJsonObject args = message["args"].toObject();
  129. QString imageFilePath = effectArray[0].toString().replace("$ROOT", _rootPath) + '/' + args.value("file").toString();
  130. QFileInfo imageFileName(imageFilePath);
  131. if (!FileUtils::writeFile(imageFileName.absoluteFilePath(), QByteArray::fromBase64(message["imageData"].toString("").toUtf8()), _log))
  132. {
  133. return "Error while saving image file '" + message["args"].toObject().value("file").toString() + ", please check the Hyperion Log";
  134. }
  135. //Update json with image file location
  136. args["file"] = imageFilePath;
  137. effectJson["args"] = args;
  138. }
  139. if (message["args"].toObject().value("imageSource").toString("") == "url" || message["args"].toObject().value("imageSource").toString("") == "file")
  140. {
  141. QJsonObject args = message["args"].toObject();
  142. args.remove(args.value("imageSource").toString("") == "url" ? "file" : "url");
  143. effectJson["args"] = args;
  144. }
  145. if (!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log))
  146. {
  147. return "Error while saving effect, please check the Hyperion Log";
  148. }
  149. Info(_log, "Reload effect list");
  150. updateEffects();
  151. resultMsg = "";
  152. }
  153. else
  154. {
  155. resultMsg = "Can't save new effect. Effect path empty";
  156. }
  157. }
  158. else
  159. {
  160. resultMsg = "Missing schema file for Python script " + message["script"].toString();
  161. }
  162. }
  163. else
  164. {
  165. resultMsg = "Missing or empty Object 'args'";
  166. }
  167. return resultMsg;
  168. }
  169. void EffectFileHandler::updateEffects()
  170. {
  171. // clear all lists
  172. _availableEffects.clear();
  173. _effectSchemas.clear();
  174. // read all effects
  175. const QJsonArray& paths = _effectConfig["paths"].toArray();
  176. const QJsonArray& disabledEfx = _effectConfig["disable"].toArray();
  177. QStringList efxPathList;
  178. efxPathList << ":/effects/";
  179. QStringList disableList;
  180. for (const auto& p : paths)
  181. {
  182. QString effectPath = p.toString();
  183. if (!effectPath.endsWith('/'))
  184. {
  185. effectPath.append('/');
  186. }
  187. efxPathList << effectPath.replace("$ROOT", _rootPath);
  188. }
  189. for (const auto& efx : disabledEfx)
  190. {
  191. disableList << efx.toString();
  192. }
  193. QMap<QString, EffectDefinition> availableEffects;
  194. for (const QString& path : std::as_const(efxPathList))
  195. {
  196. QDir directory(path);
  197. if (!directory.exists())
  198. {
  199. if (directory.mkpath(path))
  200. {
  201. Info(_log, "New Effect path \"%s\" created successfully", QSTRING_CSTR(path));
  202. }
  203. else
  204. {
  205. Warning(_log, "Failed to create Effect path \"%s\", please check permissions", QSTRING_CSTR(path));
  206. }
  207. }
  208. else
  209. {
  210. int efxCount = 0;
  211. const QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase);
  212. for (const QString& filename : filenames)
  213. {
  214. EffectDefinition def;
  215. if (loadEffectDefinition(path, filename, def))
  216. {
  217. InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log,
  218. "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path));
  219. if (disableList.contains(def.name))
  220. {
  221. Info(_log, "effect '%s' not loaded, because it is disabled in hyperion config", QSTRING_CSTR(def.name));
  222. }
  223. else
  224. {
  225. availableEffects[def.name] = def;
  226. efxCount++;
  227. }
  228. }
  229. }
  230. Info(_log, "%d effects loaded from directory %s", efxCount, QSTRING_CSTR(path));
  231. // collect effect schemas
  232. efxCount = 0;
  233. QString schemaPath = path + "schema" + '/';
  234. directory.setPath(schemaPath);
  235. const QStringList schemaFileNames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase);
  236. for (const QString& schemaFileName : schemaFileNames)
  237. {
  238. EffectSchema pyEffect;
  239. if (loadEffectSchema(path, directory.filePath(schemaFileName), pyEffect))
  240. {
  241. _effectSchemas.push_back(pyEffect);
  242. efxCount++;
  243. }
  244. }
  245. InfoIf(efxCount > 0, _log, "%d effect schemas loaded from directory %s", efxCount, QSTRING_CSTR(schemaPath));
  246. }
  247. }
  248. for (const auto& item : std::as_const(availableEffects))
  249. {
  250. _availableEffects.push_back(item);
  251. }
  252. ErrorIf(_availableEffects.empty(), _log, "no effects found, check your effect directories");
  253. emit effectListChanged();
  254. }
  255. bool EffectFileHandler::loadEffectDefinition(const QString& path, const QString& effectConfigFile, EffectDefinition& effectDefinition)
  256. {
  257. QString fileName = path + effectConfigFile;
  258. // Read and parse the effect json config file
  259. QJsonObject configEffect;
  260. if (!JsonUtils::readFile(fileName, configEffect, _log)) {
  261. return false;
  262. }
  263. // validate effect config with effect schema(path)
  264. if (!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) {
  265. return false;
  266. }
  267. // setup the definition
  268. effectDefinition.file = fileName;
  269. QJsonObject config = configEffect;
  270. QString scriptName = config["script"].toString();
  271. effectDefinition.name = config["name"].toString();
  272. if (scriptName.isEmpty()) {
  273. return false;
  274. }
  275. QFile fileInfo(scriptName);
  276. if (!fileInfo.exists())
  277. {
  278. effectDefinition.script = path + scriptName;
  279. }
  280. else
  281. {
  282. effectDefinition.script = scriptName;
  283. }
  284. effectDefinition.args = config["args"].toObject();
  285. effectDefinition.smoothCfg = 1; // pause config
  286. return true;
  287. }
  288. bool EffectFileHandler::loadEffectSchema(const QString& path, const QString& schemaFilePath, EffectSchema& effectSchema)
  289. {
  290. // Read and parse the effect schema file
  291. QJsonObject schemaEffect;
  292. if (!JsonUtils::readFile(schemaFilePath, schemaEffect, _log))
  293. {
  294. return false;
  295. }
  296. // setup the definition
  297. QString scriptName = schemaEffect["script"].toString();
  298. effectSchema.schemaFile = schemaFilePath;
  299. QString scriptFilePath = path + scriptName;
  300. QFile pyScriptFile(scriptFilePath);
  301. if (scriptName.isEmpty() || !pyScriptFile.open(QIODevice::ReadOnly))
  302. {
  303. Error(_log, "Python script '%s' in effect schema '%s' could not be loaded", QSTRING_CSTR(scriptName), QSTRING_CSTR(schemaFilePath));
  304. return false;
  305. }
  306. pyScriptFile.close();
  307. effectSchema.pyFile = scriptFilePath;
  308. effectSchema.pySchema = schemaEffect;
  309. return true;
  310. }