kapsikkum-unmanic – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3  
4 """
5 unmanic.plugins.py
6  
7 Written by: Josh.5 <jsunnex@gmail.com>
8 Date: 03 Mar 2021, (3:52 PM)
9  
10 Copyright:
11 Copyright (C) Josh Sunnex - All Rights Reserved
12  
13 Permission is hereby granted, free of charge, to any person obtaining a copy
14 of this software and associated documentation files (the "Software"), to deal
15 in the Software without restriction, including without limitation the rights
16 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 copies of the Software, and to permit persons to whom the Software is
18 furnished to do so, subject to the following conditions:
19  
20 The above copyright notice and this permission notice shall be included in all
21 copies or substantial portions of the Software.
22  
23 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
26 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
27 DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
28 OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
29 OR OTHER DEALINGS IN THE SOFTWARE.
30  
31 """
32 import base64
33 import hashlib
34 import json
35 import os
36 import shutil
37 import zipfile
38 from operator import attrgetter
39  
40 import requests
41  
42 from unmanic import config
43 from unmanic.libs import common, unlogger
44 from unmanic.libs.library import Library
45 from unmanic.libs.session import Session
46 from unmanic.libs.singleton import SingletonType
47 from unmanic.libs.unmodels import EnabledPlugins, LibraryPluginFlow, Plugins, PluginRepos
48 from unmanic.libs.unplugins import PluginExecutor
49  
50  
51 class PluginsHandler(object, metaclass=SingletonType):
52 """
53 Set plugin version.
54 Plugins must be compatible with this version to be installed.
55 """
56 version: int = 2
57  
58 def __init__(self, *args, **kwargs):
59 self.settings = config.Config()
60 unmanic_logging = unlogger.UnmanicLogger.__call__()
61 self.logger = unmanic_logging.get_logger(__class__.__name__)
62  
63 def _log(self, message, message2='', level="info"):
64 message = common.format_message(message, message2)
65 getattr(self.logger, level)(message)
66  
67 @staticmethod
68 def get_plugin_repo_id(repo_path):
69 return int(hashlib.md5(repo_path.encode('utf8')).hexdigest(), 16)
70  
71 def get_repo_cache_file(self, repo_id):
72 plugins_directory = self.settings.get_plugins_path()
73 if not os.path.exists(plugins_directory):
74 os.makedirs(plugins_directory)
75 return os.path.join(plugins_directory, "repo-{}.json".format(repo_id))
76  
77 def get_plugin_path(self, plugin_id):
78 plugin_directory = os.path.join(self.settings.get_plugins_path(), plugin_id)
79 if not os.path.exists(plugin_directory):
80 os.makedirs(plugin_directory)
81 return plugin_directory
82  
83 def get_plugin_download_cache_path(self, plugin_id, plugin_version):
84 plugin_directory = self.settings.get_plugins_path()
85 return os.path.join(plugin_directory, "{}-{}.zip".format(plugin_id, plugin_version))
86  
87 @staticmethod
88 def get_default_repo():
89 return "default"
90  
91 def get_plugin_repos(self):
92 """
93 Returns a list of plugin repos
94  
95 :return:
96 """
97 default_repo = self.get_default_repo()
98 repo_list = [
99 {
100 "path": default_repo
101 }
102 ]
103  
104 repos = PluginRepos.select().order_by(PluginRepos.id.asc())
105 for repo in repos:
106 repo_dict = repo.model_to_dict()
107 if repo_dict.get('path') == default_repo:
108 continue
109 repo_list.append(repo_dict)
110  
111 return repo_list
112  
113 def set_plugin_repos(self, repo_list):
114 # Ensure list of repo URLs is valid
115 for repo_path in repo_list:
116 repo_data = self.fetch_remote_repo_data(repo_path)
117 if not repo_data:
118 return False
119  
120 # Remove all existing repos
121 PluginRepos.delete().execute()
122  
123 # Add new repos
124 data = []
125 for repo_path in repo_list:
126 data.append({"path": repo_path})
127  
128 PluginRepos.insert_many(data).execute()
129  
130 return True
131  
132 def fetch_remote_repo_data(self, repo_path):
133 # Fetch remote JSON file
134 session = Session()
135 uuid = session.get_installation_uuid()
136 level = session.get_supporter_level()
137 repo = base64.b64encode(repo_path.encode('utf-8')).decode('utf-8')
138 api_path = f'plugin_repos/get_repo_data/uuid/{uuid}/level/{level}/repo/{repo}'
139 data, status_code = session.api_get(
140 'unmanic-api',
141 1,
142 api_path,
143 )
144 if status_code >= 500:
145 self._log(f"Failed to fetch plugin repo from '{api_path}'. Code:{status_code}", level="debug")
146 return data
147  
148 def update_plugin_repos(self):
149 """
150 Updates the local cached data of plugin repos
151  
152 :return:
153 """
154 plugins_directory = self.settings.get_plugins_path()
155 if not os.path.exists(plugins_directory):
156 os.makedirs(plugins_directory)
157 current_repos_list = self.get_plugin_repos()
158 for repo in current_repos_list:
159 repo_path = repo.get('path')
160 repo_id = self.get_plugin_repo_id(repo_path)
161  
162 # Fetch remote JSON file
163 repo_data = self.fetch_remote_repo_data(repo_path)
164  
165 # Dumb object to local JSON file
166 repo_cache = self.get_repo_cache_file(repo_id)
167 self._log("Repo cache file '{}'.".format(repo_cache), level="info")
168 try:
169 with open(repo_cache, 'w') as f:
170 json.dump(repo_data, f, indent=4)
171 except json.JSONDecodeError as e:
172 self._log("Unable to update plugin repo '{}'.".format(repo_path), str(e), level="error")
173 return True
174  
175 def get_settings_of_all_installed_plugins(self):
176 all_settings = {}
177  
178 # First fetch all enabled plugins
179 order = [
180 {
181 "column": 'name',
182 "dir": 'asc',
183 },
184 ]
185 installed_plugins = self.get_plugin_list_filtered_and_sorted(order=order)
186  
187 # Fetch settings for each plugin
188 plugin_executor = PluginExecutor()
189 for plugin in installed_plugins:
190 plugin_settings, plugin_settings_meta = plugin_executor.get_plugin_settings(plugin.get('plugin_id'))
191 all_settings[plugin.get('plugin_id')] = plugin_settings
192  
193 # Return modules
194 return all_settings
195  
196 def read_repo_data(self, repo_id):
197 repo_cache = self.get_repo_cache_file(repo_id)
198 if os.path.exists(repo_cache):
199 with open(repo_cache) as f:
200 repo_data = json.load(f)
201 return repo_data
202 return {}
203  
204 def get_plugin_info(self, plugin_id):
205 plugin_info = {}
206 plugin_directory = os.path.join(self.settings.get_plugins_path(), plugin_id)
207 info_file = os.path.join(plugin_directory, 'info.json')
208 if os.path.exists(info_file):
209 # Read plugin info.json
210 with open(info_file) as json_file:
211 plugin_info = json.load(json_file)
212 return plugin_info
213  
214 def get_plugins_in_repo_data(self, repo_data):
215 return_list = []
216 if 'repo' in repo_data and 'plugins' in repo_data:
217 # Get URLs for plugin downloads
218 repo_meta = repo_data.get("repo")
219 repo_data_directory = repo_meta.get("repo_data_directory")
220 if not repo_data_directory:
221 return return_list
222 repo_data_directory = repo_data_directory.rstrip('/')
223 # if not repo_data_directory.endswith("/"):
224 # repo_data_directory = repo_data_directory + "/"
225  
226 # Loop over
227 for plugin in repo_data.get("plugins", []):
228 # Only show plugins that are compatible with this version
229 # Plugins will require a 'compatibility' entry in their info.json file.
230 # This must list the plugin handler versions that it is compatible with
231 if self.version not in plugin.get('compatibility', []):
232 continue
233  
234 plugin_package_url = "{0}/{1}/{1}-{2}.zip".format(repo_data_directory, plugin.get('id'), plugin.get('version'))
235 plugin_changelog_url = "{0}/{1}/changelog.md".format(repo_data_directory, plugin.get('id'))
236  
237 # Check if plugin is already installed:
238 plugin_status = {
239 'installed': False,
240 }
241 plugin_info = self.get_plugin_info(plugin.get('id'))
242 if plugin_info:
243 local_version = plugin_info.get('version')
244 # Parse the currently installed version number and check if it matches
245 remote_version = plugin.get('version')
246 if local_version == remote_version:
247 plugin_status = {
248 'installed': True,
249 'update_available': False,
250 }
251 else:
252 # There is an update available
253 self.flag_plugin_for_update_by_id(plugin.get("id"))
254 plugin_status = {
255 'installed': True,
256 'update_available': True,
257 }
258  
259 return_list.append(
260 {
261 'plugin_id': plugin.get('id'),
262 'name': plugin.get('name'),
263 'author': plugin.get('author'),
264 'description': plugin.get('description'),
265 'version': plugin.get('version'),
266 'icon': plugin.get('icon', ''),
267 'tags': plugin.get('tags'),
268 'status': plugin_status,
269 'package_url': plugin_package_url,
270 'changelog_url': plugin_changelog_url,
271 'repo_name': repo_meta.get('name'),
272 }
273 )
274 return return_list
275  
276 def get_installable_plugins_list(self, filter_repo_id=None):
277 """
278 Return a list of plugins that can be installed
279 Optionally filter by repo
280  
281 :param filter_repo_id:
282 :return:
283 """
284 return_list = []
285  
286 # First fetch a list of available repos
287 current_repos_list = self.get_plugin_repos()
288 for repo in current_repos_list:
289 repo_path = repo.get('path')
290 repo_id = self.get_plugin_repo_id(repo_path)
291 if filter_repo_id and repo_id != int(filter_repo_id):
292 # Filtering by repo ID and this one does not match
293 continue
294 repo_data = self.read_repo_data(repo_id)
295 plugins_in_repo = self.get_plugins_in_repo_data(repo_data)
296 for plugin_data in plugins_in_repo:
297 plugin_data['repo_id'] = str(repo_id)
298 return_list += plugins_in_repo
299  
300 return return_list
301  
302 def read_remote_changelog_file(self, changelog_url):
303 r = requests.get(changelog_url, timeout=1)
304 if r.status_code == 200:
305 return r.text
306 return ''
307  
308 def notify_site_of_plugin_install(self, plugin):
309 """
310 Notify the unmanic.app site API of the installation.
311 This is used for metric stats so that we can get a count of plugin downloads.
312  
313 :param plugin:
314 :return:
315 """
316 # Post
317 session = Session()
318 uuid = session.get_installation_uuid()
319 level = session.get_supporter_level()
320 post_data = {
321 "uuid": uuid,
322 "level": level,
323 "plugin_id": plugin.get("plugin_id"),
324 "author": plugin.get("author"),
325 "version": plugin.get("version"),
326 }
327 try:
328 repo_data, status_code = session.api_post('unmanic-api', 1, 'plugin_repos/record_install', post_data)
329 if not repo_data.get('success'):
330 session.register_unmanic()
331 except Exception as e:
332 self._log("Exception while logging plugin install.", str(e), level="debug")
333 return False
334  
335 def install_plugin_by_id(self, plugin_id, repo_id=None):
336 """
337 Find the matching plugin info for the given plugin ID.
338 Download the plugin if it is found and return the result.
339 If it is not found, return False.
340  
341 :param plugin_id:
342 :param repo_id:
343 :return:
344 """
345 plugin_list = self.get_installable_plugins_list(filter_repo_id=repo_id)
346 for plugin in plugin_list:
347 if plugin.get('plugin_id') == plugin_id:
348 success = self.download_and_install_plugin(plugin)
349  
350 if success:
351 try:
352 # Write the plugin info to the DB
353 plugin_directory = self.get_plugin_path(plugin.get("plugin_id"))
354 result = self.write_plugin_data_to_db(plugin, plugin_directory)
355 if result:
356 self._log("Installed plugin '{}'".format(plugin_id), level="info")
357  
358 # Ensure the plugin module is reloaded (if it was previously loaded)
359 plugin_executor = PluginExecutor()
360 plugin_executor.reload_plugin_module(plugin.get('plugin_id'))
361  
362 return result
363 except Exception as e:
364 self._log("Exception while installing plugin '{}'.".format(plugin), str(e), level="exception")
365  
366 return False
367  
368 def install_plugin_from_path_on_disk(self, abspath):
369 """
370 Install a plugin from a ZIP file on disk
371  
372 :param abspath:
373 :return:
374 """
375 # TODO: Ensure that this is a zip file
376 try:
377 plugin_info = self.install_plugin(abspath)
378  
379 # Set the plugin_id variable used when writing data to DB.
380 # The returned 'plugin_info' is just a readout of the info.json file which has this set to 'id'
381 plugin_info['plugin_id'] = plugin_info.get('id')
382  
383 # Cleanup zip file
384 if os.path.isfile(abspath):
385 os.remove(abspath)
386  
387 # Write the plugin info to the DB
388 plugin_directory = self.get_plugin_path(plugin_info.get("plugin_id"))
389 result = self.write_plugin_data_to_db(plugin_info, plugin_directory)
390 if result:
391 self._log("Installed plugin '{}'".format(plugin_info.get("plugin_id")), level="info")
392  
393 # Ensure the plugin module is reloaded (if it was previously loaded)
394 plugin_executor = PluginExecutor()
395 plugin_executor.reload_plugin_module(plugin_info.get('plugin_id'))
396  
397 return result
398 except Exception as e:
399 self._log("Exception while installing plugin from zip '{}'.".format(abspath), str(e), level="exception")
400  
401 return False
402  
403 def download_and_install_plugin(self, plugin):
404 """
405 Download and install a given plugin
406  
407 :param plugin:
408 :return:
409 """
410 self._log("Installing plugin '{}'".format(plugin.get("name")), level='debug')
411 # Try to fetch URL
412 try:
413 # Fetch remote zip file
414 destination = self.download_plugin(plugin)
415  
416 # Install downloaded plugin
417 self.install_plugin(destination, plugin.get("plugin_id"))
418  
419 # Cleanup zip file
420 if os.path.isfile(destination):
421 os.remove(destination)
422  
423 self.notify_site_of_plugin_install(plugin)
424  
425 return True
426  
427 except Exception as e:
428 success = False
429 self._log("Exception while installing plugin '{}'.".format(plugin), str(e), level="exception")
430  
431 return False
432  
433 def download_plugin(self, plugin):
434 """
435 Download a given plugin to a temp directory
436  
437 :param plugin:
438 :return:
439 """
440 # Fetch remote zip file
441 destination = self.get_plugin_download_cache_path(plugin.get("plugin_id"), plugin.get("version"))
442 self._log("Downloading plugin '{}' to '{}'".format(plugin.get("package_url"), destination), level='debug')
443 with requests.get(plugin.get("package_url"), stream=True, allow_redirects=True) as r:
444 r.raise_for_status()
445 with open(destination, 'wb') as f:
446 for chunk in r.iter_content(chunk_size=128):
447 f.write(chunk)
448 return destination
449  
450 def install_plugin(self, zip_file, plugin_id=None):
451 """
452 Install a given plugin from a zip file
453  
454 :param zip_file:
455 :param plugin_id:
456 :return:
457 """
458 # Read plugin ID from zip contents info.json if no plugin_id was provided
459 if not plugin_id:
460 with zipfile.ZipFile(zip_file, "r") as zip_ref:
461 plugin_info = json.loads(zip_ref.read('info.json'))
462 plugin_id = plugin_info.get('id')
463 # Create plugin destination directory based on plugin ID
464 plugin_directory = self.get_plugin_path(plugin_id)
465 # Prevent installation if destination has a git repository. This plugin is probably under development
466 self._log(os.path.join(str(plugin_directory), '.git'))
467 if os.path.exists(os.path.join(str(plugin_directory), '.git')):
468 raise Exception("Plugin directory contains a git repository. Uninstall this source version before installing.")
469 # Extract zip file contents
470 self._log("Extracting plugin to '{}'".format(plugin_directory), level='debug')
471 with zipfile.ZipFile(zip_file, "r") as zip_ref:
472 zip_ref.extractall(str(plugin_directory))
473 # Return installed plugin info
474 return self.get_plugin_info(plugin_id)
475  
476 @staticmethod
477 def write_plugin_data_to_db(plugin, plugin_directory):
478 # Add installed plugin to database
479 plugin_data = {
480 Plugins.plugin_id: plugin.get("plugin_id"),
481 Plugins.name: plugin.get("name"),
482 Plugins.author: plugin.get("author"),
483 Plugins.version: plugin.get("version"),
484 Plugins.tags: plugin.get("tags"),
485 Plugins.description: plugin.get("description"),
486 Plugins.icon: plugin.get("icon"),
487 Plugins.local_path: plugin_directory,
488 Plugins.update_available: False,
489 }
490 plugin_entry = Plugins.get_or_none(plugin_id=plugin.get("plugin_id"))
491 if plugin_entry is not None:
492 # Update the existing entry
493 update_query = (Plugins
494 .update(plugin_data)
495 .where(Plugins.plugin_id == plugin.get("plugin_id")))
496 update_query.execute()
497 else:
498 # Insert a new entry
499 Plugins.insert(plugin_data).execute()
500  
501 return True
502  
503 def get_total_plugin_list_count(self):
504 task_query = Plugins.select().order_by(Plugins.id.desc())
505 return task_query.count()
506  
507 def get_plugin_list_filtered_and_sorted(self, order=None, start=0, length=None, search_value=None, id_list=None,
508 enabled=None, plugin_id=None, plugin_type=None, library_id=None):
509 try:
510 query = (Plugins.select())
511  
512 if plugin_type:
513 if library_id is not None:
514 join_condition = (
515 (LibraryPluginFlow.plugin_id == Plugins.id) & (LibraryPluginFlow.plugin_type == plugin_type) & (
516 LibraryPluginFlow.library_id == library_id))
517 else:
518 join_condition = (
519 (LibraryPluginFlow.plugin_id == Plugins.id) & (LibraryPluginFlow.plugin_type == plugin_type))
520 query = query.join(LibraryPluginFlow, join_type='LEFT OUTER JOIN', on=join_condition)
521  
522 if id_list:
523 query = query.where(Plugins.id.in_(id_list))
524  
525 if search_value:
526 query = query.where((Plugins.name.contains(search_value)) | (Plugins.author.contains(search_value)) | (
527 Plugins.tags.contains(search_value)))
528  
529 if plugin_id is not None:
530 query = query.where(Plugins.plugin_id.in_([plugin_id]))
531  
532 # Deprecate this "enabled" status as plugins are now enabled when the are assigned to a library
533 if enabled is not None:
534 raise Exception("Fetching plugins by 'enabled' status is deprecated")
535  
536 if library_id is not None:
537 join_condition = (
538 (EnabledPlugins.plugin_id == Plugins.id) & (EnabledPlugins.library_id == library_id))
539 query = query.join(EnabledPlugins, join_type='LEFT OUTER JOIN', on=join_condition)
540 query = query.where(EnabledPlugins.plugin_id != None)
541  
542 # Get order by
543 if order:
544 for o in order:
545 if o.get("model"):
546 model = o.get("model")
547 else:
548 model = Plugins
549 if o.get("dir") == "asc":
550 order_by = attrgetter(o.get("column"))(model).asc()
551 else:
552 order_by = attrgetter(o.get("column"))(model).desc()
553  
554 query = query.order_by_extend(order_by)
555  
556 if length:
557 query = query.limit(length).offset(start)
558  
559 return query.dicts()
560  
561 except Plugins.DoesNotExist:
562 # No plugin entries exist yet
563 self._log("No plugins exist yet.", level="warning")
564  
565 def flag_plugin_for_update_by_id(self, plugin_id):
566 self._log("Flagging update available for installed plugin '{}'".format(plugin_id), level='debug')
567 # Disable the matching entries in the table
568 Plugins.update(update_available=True).where(Plugins.plugin_id == plugin_id).execute()
569  
570 # Fetch records
571 records = self.get_plugin_list_filtered_and_sorted(plugin_id=plugin_id)
572  
573 # Ensure they are now disabled
574 for record in records:
575 if record.get('update_available'):
576 continue
577 self._log("Failed to flag plugin for update '{}'".format(record.get('plugin_id')), level='debug')
578 return False
579  
580 return True
581  
582 def uninstall_plugins_by_db_table_id(self, plugin_table_ids: list):
583 """
584 Remove a Plugin by it's DB table ID column.
585 This will also remove the Plugin directory and all it's contents.
586  
587 :param plugin_table_ids:
588 :return:
589 """
590 self._log("Uninstall plugins '{}'".format(plugin_table_ids), level='debug')
591  
592 # Fetch records
593 records_by_id = self.get_plugin_list_filtered_and_sorted(id_list=plugin_table_ids)
594  
595 # Remove each plugin from disk
596 for record in records_by_id:
597 # Unload plugin modules
598 try:
599 PluginExecutor.unload_plugin_module(record.get('plugin_id'))
600 except Exception as e:
601 self._log("Exception while unloading python module {}:".format(record.get('plugin_id')), message2=str(e),
602 level="exception")
603  
604 # Remove from disk
605 plugin_directory = self.get_plugin_path(record.get('plugin_id'))
606 self._log("Removing plugin files from disk '{}'".format(plugin_directory), level='debug')
607 try:
608 # Delete the info file first to prevent any other process trying to read the plugin.
609 # Without the info file, the plugin is effectivly uninstalled
610 info_file = os.path.join(plugin_directory, 'info.json')
611 if os.path.exists(info_file):
612 os.remove(info_file)
613 # Cleanup the rest of the plugin directory
614 shutil.rmtree(plugin_directory)
615 except Exception as e:
616 self._log("Exception while removing directory {}:".format(plugin_directory), message2=str(e),
617 level="exception")
618  
619 # Unlink from library by ID in DB
620 EnabledPlugins.delete().where(EnabledPlugins.plugin_id.in_(plugin_table_ids)).execute()
621  
622 # Delete by ID in DB
623 if not Plugins.delete().where(Plugins.id.in_(plugin_table_ids)).execute():
624 return False
625  
626 return True
627  
628 def update_plugins_by_db_table_id(self, plugin_table_ids):
629 self._log("Update plugins '{}'".format(plugin_table_ids), level='debug')
630  
631 # Fetch records
632 records_by_id = self.get_plugin_list_filtered_and_sorted(id_list=plugin_table_ids)
633  
634 # Update each plugin in turn
635 for record in records_by_id:
636 if self.install_plugin_by_id(record.get('plugin_id')):
637 continue
638 self._log("Failed to update plugin '{}'".format(record.get('plugin_id')), level='debug')
639 return False
640  
641 return True
642  
643 def set_plugin_flow(self, plugin_type, library_id, flow):
644 """
645 Update the plugin flow for all plugins in a given plugin type
646  
647 :param plugin_type:
648 :param library_id:
649 :param flow:
650 :return:
651 """
652 # Delete all current flow data for this plugin type
653 delete_query = LibraryPluginFlow.delete().where(
654 (LibraryPluginFlow.plugin_type == plugin_type) & (LibraryPluginFlow.library_id == library_id))
655 delete_query.execute()
656  
657 success = True
658 priority = 1
659 for plugin in flow:
660 plugin_id = plugin.get('plugin_id')
661  
662 # Fetch the plugin info
663 plugin_info = Plugins.select().where(Plugins.plugin_id == plugin_id).first()
664 if not plugin_info:
665 continue
666  
667 # Save the plugin flow
668 plugin_flow = self.set_plugin_flow_position_for_single_plugin(plugin_info, plugin_type, library_id, priority)
669 priority += 1
670  
671 if not plugin_flow:
672 success = False
673  
674 return success
675  
676 @staticmethod
677 def set_plugin_flow_position_for_single_plugin(plugin_info: Plugins, plugin_type: str, library_id: int, priority: int):
678 """
679 Update the plugin flow for a single plugin and type with the provided priority.
680  
681 :param plugin_info:
682 :param plugin_type:
683 :param library_id:
684 :param priority:
685 :return:
686 """
687 pass
688 # Save the plugin flow
689 flow_dict = {
690 'plugin_id': plugin_info.id,
691 'library_id': library_id,
692 'plugin_name': plugin_info.plugin_id,
693 'plugin_type': plugin_type,
694 'position': priority,
695 }
696 plugin_flow = LibraryPluginFlow.create(**flow_dict)
697  
698 return plugin_flow
699  
700 def get_enabled_plugin_modules_by_type(self, plugin_type, library_id=None):
701 """
702 Return a list of enabled plugin modules when given a plugin type
703  
704 Runners are filtered by the given 'plugin_type' and sorted by
705 configured order of execution.
706  
707 If no library ID is provided, this will return all installed plugins for that type.
708 This case should only be used for plugin runner types that are not associated with a library.
709  
710 :param plugin_type:
711 :param library_id:
712 :return:
713 """
714 # Refresh session
715 s = Session()
716 s.register_unmanic()
717  
718 # First fetch all enabled plugins
719 order = [
720 {
721 "model": LibraryPluginFlow,
722 "column": 'position',
723 "dir": 'asc',
724 },
725 {
726 "column": 'name',
727 "dir": 'asc',
728 },
729 ]
730 enabled_plugins = self.get_plugin_list_filtered_and_sorted(order=order, plugin_type=plugin_type, library_id=library_id)
731  
732 # Fetch all plugin modules from the given list of enabled plugins
733 plugin_executor = PluginExecutor()
734 plugin_data = plugin_executor.get_plugin_data_by_type(enabled_plugins, plugin_type)
735  
736 # Return modules
737 return plugin_data
738  
739 def exec_plugin_runner(self, data, plugin_id, plugin_type):
740 """
741 Execute a plugin runner
742  
743 :param data:
744 :param plugin_id:
745 :param plugin_type:
746 :return:
747 """
748 plugin_executor = PluginExecutor()
749 return plugin_executor.execute_plugin_runner(data, plugin_id, plugin_type)
750  
751 def get_incompatible_enabled_plugins(self, frontend_messages=None):
752 """
753 Ensure that the currently installed plugins are compatible with this PluginsHandler version
754  
755 :param frontend_messages:
756 :return:
757 :rtype:
758 """
759 # Fetch all libraries
760 all_libraries = Library.get_all_libraries()
761  
762 def add_frontend_message(plugin_id, name):
763 # If the frontend messages queue was included in request, append a message
764 if frontend_messages:
765 frontend_messages.put(
766 {
767 'id': 'incompatiblePlugin_{}'.format(plugin_id),
768 'type': 'error',
769 'code': 'incompatiblePlugin',
770 'message': name,
771 'timeout': 0
772 }
773 )
774  
775 # Fetch all enabled plugins
776 incompatible_list = []
777 for library in all_libraries:
778 enabled_plugins = self.get_plugin_list_filtered_and_sorted(library_id=library.get('id'))
779  
780 # Ensure only compatible plugins are enabled
781 # If all enabled plugins are compatible, then return true
782 for record in enabled_plugins:
783 try:
784 # Ensure plugin is compatible
785 plugin_info = self.get_plugin_info(record.get('plugin_id'))
786 except Exception as e:
787 plugin_info = None
788 self._log("Exception while fetching plugin info for {}:".format(record.get('plugin_id')), message2=str(e),
789 level="exception")
790 if plugin_info:
791 # Plugins will require a 'compatibility' entry in their info.json file.
792 # This must list the plugin handler versions that it is compatible with
793 if self.version in plugin_info.get('compatibility', []):
794 continue
795  
796 incompatible_list.append(
797 {
798 'plugin_id': record.get('plugin_id'),
799 'name': record.get('name'),
800 }
801 )
802 add_frontend_message(record.get('plugin_id'), record.get('name'))
803  
804 return incompatible_list
805  
806 @staticmethod
807 def get_plugin_types_with_flows():
808 """
809 Returns a list of all available plugin types
810  
811 :return:
812 """
813 return_plugin_types = []
814 plugin_ex = PluginExecutor()
815 types_list = plugin_ex.get_all_plugin_types()
816 # Filter out the types without flows
817 for plugin_type in types_list:
818 if plugin_type.get('has_flow'):
819 return_plugin_types.append(plugin_type.get('id'))
820 return return_plugin_types
821  
822 def get_enabled_plugin_flows_for_plugin_type(self, plugin_type, library_id):
823 """
824 Fetch all enabled plugin flows for a plugin type
825  
826 :param plugin_type:
827 :param library_id:
828 :return:
829 """
830 return_plugin_flow = []
831 for plugin_module in self.get_enabled_plugin_modules_by_type(plugin_type, library_id=library_id):
832 return_plugin_flow.append(
833 {
834 "plugin_id": plugin_module.get("plugin_id"),
835 "name": plugin_module.get("name", ""),
836 "author": plugin_module.get("author", ""),
837 "description": plugin_module.get("description", ""),
838 "version": plugin_module.get("version", ""),
839 "icon": plugin_module.get("icon", ""),
840 }
841 )
842 return return_plugin_flow